Speed up navigation (#824)

* use client-side navigation

* update tests

* add setting for enabling link prefetching

* do not prefetch bookmark details

* theme progress bar

* cleanup behaviors

* update test
This commit is contained in:
Sascha Ißbrücker 2024-09-14 11:32:19 +02:00 committed by GitHub
parent 3ae9cf0420
commit c929e8f11c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 283 additions and 144 deletions

View file

@ -16,9 +16,13 @@ const mutationObserver = new MutationObserver((mutations) => {
}); });
}); });
mutationObserver.observe(document.body, { window.addEventListener("turbo:load", () => {
childList: true, mutationObserver.observe(document.body, {
subtree: true, childList: true,
subtree: true,
});
applyBehaviors(document.body);
}); });
export class Behavior { export class Behavior {

View file

@ -1,3 +1,4 @@
import "@hotwired/turbo";
import "./behaviors/bookmark-page"; import "./behaviors/bookmark-page";
import "./behaviors/bulk-edit"; import "./behaviors/bulk-edit";
import "./behaviors/confirm-button"; import "./behaviors/confirm-button";

View file

@ -8,28 +8,33 @@ class CustomRemoteUserMiddleware(RemoteUserMiddleware):
header = settings.LD_AUTH_PROXY_USERNAME_HEADER header = settings.LD_AUTH_PROXY_USERNAME_HEADER
default_global_settings = GlobalSettings()
standard_profile = UserProfile() standard_profile = UserProfile()
standard_profile.enable_favicons = True standard_profile.enable_favicons = True
class UserProfileMiddleware: class LinkdingMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
# add global settings to request
try:
global_settings = GlobalSettings.get()
except:
global_settings = default_global_settings
request.global_settings = global_settings
# add user profile to request
if request.user.is_authenticated: if request.user.is_authenticated:
request.user_profile = request.user.profile request.user_profile = request.user.profile
else: else:
# check if a custom profile for guests exists, otherwise use standard profile # check if a custom profile for guests exists, otherwise use standard profile
guest_profile = None if global_settings.guest_profile_user:
try: request.user_profile = global_settings.guest_profile_user.profile
global_settings = GlobalSettings.get() else:
if global_settings.guest_profile_user: request.user_profile = standard_profile
guest_profile = global_settings.guest_profile_user.profile
except:
pass
request.user_profile = guest_profile or standard_profile
response = self.get_response(request) response = self.get_response(request)

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.8 on 2024-09-14 07:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookmarks", "0038_globalsettings_guest_profile_user"),
]
operations = [
migrations.AddField(
model_name="globalsettings",
name="enable_link_prefetch",
field=models.BooleanField(default=False),
),
]

View file

@ -514,6 +514,7 @@ class GlobalSettings(models.Model):
guest_profile_user = models.ForeignKey( guest_profile_user = models.ForeignKey(
get_user_model(), on_delete=models.SET_NULL, null=True, blank=True get_user_model(), on_delete=models.SET_NULL, null=True, blank=True
) )
enable_link_prefetch = models.BooleanField(default=False, null=False)
@classmethod @classmethod
def get(cls): def get(cls):
@ -532,7 +533,7 @@ class GlobalSettings(models.Model):
class GlobalSettingsForm(forms.ModelForm): class GlobalSettingsForm(forms.ModelForm):
class Meta: class Meta:
model = GlobalSettings model = GlobalSettings
fields = ["landing_page", "guest_profile_user"] fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(GlobalSettingsForm, self).__init__(*args, **kwargs) super(GlobalSettingsForm, self).__init__(*args, **kwargs)

View file

@ -57,4 +57,9 @@ span.confirmation {
.divider { .divider {
border-bottom: solid 1px var(--secondary-border-color); border-bottom: solid 1px var(--secondary-border-color);
margin: var(--unit-5) 0; margin: var(--unit-5) 0;
} }
/* Turbo progress bar */
.turbo-progress-bar {
background-color: var(--primary-color);
}

View file

@ -26,7 +26,7 @@
{% if bookmark_list.show_url %} {% if bookmark_list.show_url %}
<div class="url-path truncate"> <div class="url-path truncate">
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener" <a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
class="url-display"> class="url-display">
{{ bookmark_item.url }} {{ bookmark_item.url }}
</a> </a>
</div> </div>
@ -66,9 +66,9 @@
{% if bookmark_item.display_date %} {% if bookmark_item.display_date %}
{% if bookmark_item.web_archive_snapshot_url %} {% if bookmark_item.web_archive_snapshot_url %}
<a href="{{ bookmark_item.web_archive_snapshot_url }}" <a href="{{ bookmark_item.web_archive_snapshot_url }}"
title="Show snapshot on the Internet Archive Wayback Machine" title="Show snapshot on the Internet Archive Wayback Machine"
target="{{ bookmark_list.link_target }}" target="{{ bookmark_list.link_target }}"
rel="noopener"> rel="noopener">
{{ bookmark_item.display_date }} {{ bookmark_item.display_date }}
</a> </a>
{% else %} {% else %}
@ -79,8 +79,9 @@
{# View link is visible for both owned and shared bookmarks #} {# View link is visible for both owned and shared bookmarks #}
{% if bookmark_list.show_view_action %} {% if bookmark_list.show_view_action %}
<a ld-fetch="{% url 'bookmarks:details_modal' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}" <a ld-fetch="{% url 'bookmarks:details_modal' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}"
ld-on="click" ld-target="body|append" ld-on="click" ld-target="body|append"
href="{% url 'bookmarks:details' bookmark_item.id %}">View</a> data-turbo-prefetch="false"
href="{% url 'bookmarks:details' bookmark_item.id %}">View</a>
{% endif %} {% endif %}
{% if bookmark_item.is_editable %} {% if bookmark_item.is_editable %}
{# Bookmark owner actions #} {# Bookmark owner actions #}

View file

@ -35,6 +35,11 @@
{% if request.user_profile.custom_css %} {% if request.user_profile.custom_css %}
<style>{{ request.user_profile.custom_css }}</style> <style>{{ request.user_profile.custom_css }}</style>
{% endif %} {% endif %}
<meta name="turbo-cache-control" content="no-preview">
{% if not request.global_settings.enable_link_prefetch %}
<meta name="turbo-prefetch" content="false">
{% endif %}
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
</head> </head>
<body ld-global-shortcuts> <body ld-global-shortcuts>
@ -129,6 +134,5 @@
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div> </div>
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
</body> </body>
</html> </html>

View file

@ -77,7 +77,7 @@
{# Replace search input with auto-complete component #} {# Replace search input with auto-complete component #}
<script type="application/javascript"> <script type="application/javascript">
window.addEventListener("load", function () { (function init() {
const currentTagsString = '{{ tags_string }}'; const currentTagsString = '{{ tags_string }}';
const currentTags = currentTagsString.split(' '); const currentTags = currentTagsString.split(' ');
const uniqueTags = [...new Set(currentTags)] const uniqueTags = [...new Set(currentTags)]
@ -104,5 +104,5 @@
} }
}) })
input.replaceWith(wrapper.firstElementChild); input.replaceWith(wrapper.firstElementChild);
}); })();
</script> </script>

View file

@ -19,7 +19,7 @@
<p> <p>
<a href="{% url 'change_password' %}">Change password</a> <a href="{% url 'change_password' %}">Change password</a>
</p> </p>
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate> <form action="{% url 'bookmarks:settings.update' %}" method="post" novalidate data-turbo="false">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label> <label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
@ -247,7 +247,7 @@ reddit.com/r/Music music reddit</pre>
{% if global_settings_form %} {% if global_settings_form %}
<section class="content-area"> <section class="content-area">
<h2>Global settings</h2> <h2>Global settings</h2>
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate> <form action="{% url 'bookmarks:settings.update' %}" method="post" novalidate data-turbo="false">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="{{ global_settings_form.landing_page.id_for_label }}" class="form-label">Landing page</label> <label for="{{ global_settings_form.landing_page.id_for_label }}" class="form-label">Landing page</label>
@ -266,6 +266,16 @@ reddit.com/r/Music music reddit</pre>
a dedicated user for this purpose. By default, a standard profile with fixed settings is used. a dedicated user for this purpose. By default, a standard profile with fixed settings is used.
</div> </div>
</div> </div>
<div class="form-group">
<label for="{{ global_settings_form.enable_link_prefetch.id_for_label }}" class="form-checkbox">
{{ global_settings_form.enable_link_prefetch }}
<i class="form-icon"></i> Enable prefetching links on hover
</label>
<div class="form-input-hint">
Prefetches internal links when hovering over them. This can improve the perceived performance when
navigating application, but also increases the load on the server as well as bandwidth usage.
</div>
</div>
<div class="form-group"> <div class="form-group">
<input type="submit" name="update_global_settings" value="Save" class="btn btn-primary btn-wide mt-2"> <input type="submit" name="update_global_settings" value="Save" class="btn btn-primary btn-wide mt-2">
@ -306,7 +316,7 @@ reddit.com/r/Music music reddit</pre>
<section class="content-area"> <section class="content-area">
<h2>Export</h2> <h2>Export</h2>
<p>Export all bookmarks in Netscape HTML format.</p> <p>Export all bookmarks in Netscape HTML format.</p>
<a class="btn btn-primary" href="{% url 'bookmarks:settings.export' %}">Download (.html)</a> <a class="btn btn-primary" target="_blank" href="{% url 'bookmarks:settings.export' %}">Download (.html)</a>
{% if export_error %} {% if export_error %}
<div class="has-error"> <div class="has-error">
<p class="form-input-hint"> <p class="form-input-hint">
@ -344,35 +354,37 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
<script> <script>
const enableSharing = document.getElementById("{{ form.enable_sharing.id_for_label }}"); (function init() {
const enablePublicSharing = document.getElementById("{{ form.enable_public_sharing.id_for_label }}"); const enableSharing = document.getElementById("{{ form.enable_sharing.id_for_label }}");
const bookmarkDescriptionDisplay = document.getElementById("{{ form.bookmark_description_display.id_for_label }}"); const enablePublicSharing = document.getElementById("{{ form.enable_public_sharing.id_for_label }}");
const bookmarkDescriptionMaxLines = document.getElementById("{{ form.bookmark_description_max_lines.id_for_label }}"); const bookmarkDescriptionDisplay = document.getElementById("{{ form.bookmark_description_display.id_for_label }}");
const bookmarkDescriptionMaxLines = document.getElementById("{{ form.bookmark_description_max_lines.id_for_label }}");
// Automatically disable public bookmark sharing if bookmark sharing is disabled // Automatically disable public bookmark sharing if bookmark sharing is disabled
function updatePublicSharing() { function updatePublicSharing() {
if (enableSharing.checked) { if (enableSharing.checked) {
enablePublicSharing.disabled = false; enablePublicSharing.disabled = false;
} else { } else {
enablePublicSharing.disabled = true; enablePublicSharing.disabled = true;
enablePublicSharing.checked = false; enablePublicSharing.checked = false;
}
} }
}
updatePublicSharing(); updatePublicSharing();
enableSharing.addEventListener("change", updatePublicSharing); enableSharing.addEventListener("change", updatePublicSharing);
// Automatically hide the bookmark description max lines input if the description display is set to inline // Automatically hide the bookmark description max lines input if the description display is set to inline
function updateBookmarkDescriptionMaxLines() { function updateBookmarkDescriptionMaxLines() {
if (bookmarkDescriptionDisplay.value === "inline") { if (bookmarkDescriptionDisplay.value === "inline") {
bookmarkDescriptionMaxLines.closest(".form-group").classList.add("d-hide"); bookmarkDescriptionMaxLines.closest(".form-group").classList.add("d-hide");
} else { } else {
bookmarkDescriptionMaxLines.closest(".form-group").classList.remove("d-hide"); bookmarkDescriptionMaxLines.closest(".form-group").classList.remove("d-hide");
}
} }
}
updateBookmarkDescriptionMaxLines(); updateBookmarkDescriptionMaxLines();
bookmarkDescriptionDisplay.addEventListener("change", updateBookmarkDescriptionMaxLines); bookmarkDescriptionDisplay.addEventListener("change", updateBookmarkDescriptionMaxLines);
})();
</script> </script>
{% endblock %} {% endblock %}

View file

@ -52,10 +52,10 @@
<h2>RSS Feeds</h2> <h2>RSS Feeds</h2>
<p>The following URLs provide RSS feeds for your bookmarks:</p> <p>The following URLs provide RSS feeds for your bookmarks:</p>
<ul style="list-style-position: outside;"> <ul style="list-style-position: outside;">
<li><a href="{{ all_feed_url }}">All bookmarks</a></li> <li><a target="_blank" href="{{ all_feed_url }}">All bookmarks</a></li>
<li><a href="{{ unread_feed_url }}">Unread bookmarks</a></li> <li><a target="_blank" href="{{ unread_feed_url }}">Unread bookmarks</a></li>
<li><a href="{{ shared_feed_url }}">Shared bookmarks</a></li> <li><a target="_blank" href="{{ shared_feed_url }}">Shared bookmarks</a></li>
<li><a href="{{ public_shared_feed_url }}">Public shared bookmarks</a><br><span class="text-small text-secondary">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span> <li><a target="_blank" href="{{ public_shared_feed_url }}">Public shared bookmarks</a><br><span class="text-small text-secondary">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span>
</li> </li>
</ul> </ul>
<p> <p>
@ -80,7 +80,7 @@
credential.</strong> credential.</strong>
Any party with access to these URLs can read all your bookmarks. Any party with access to these URLs can read all your bookmarks.
If you think that a URL was compromised you can delete the feed token for your user in the <a If you think that a URL was compromised you can delete the feed token for your user in the <a
href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel</a>. target="_blank" href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel</a>.
After deleting the feed token, new URLs will be generated when you reload this settings page. After deleting the feed token, new URLs will be generated when you reload this settings page.
</p> </p>
</section> </section>

View file

@ -1,10 +1,10 @@
from django.contrib.auth.models import User from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from django.test import TransactionTestCase from django.test import TransactionTestCase
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
from django.urls import reverse from django.urls import reverse
from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from bookmarks.models import GlobalSettings
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
@ -20,9 +20,12 @@ class BookmarkArchivedViewPerformanceTestCase(
return connections[DEFAULT_DB_ALIAS] return connections[DEFAULT_DB_ALIAS]
def test_should_not_increase_number_of_queries_per_bookmark(self): def test_should_not_increase_number_of_queries_per_bookmark(self):
# create global settings
GlobalSettings.get()
# create initial bookmarks # create initial bookmarks
num_initial_bookmarks = 10 num_initial_bookmarks = 10
for index in range(num_initial_bookmarks): for _ in range(num_initial_bookmarks):
self.setup_bookmark(user=self.user, is_archived=True) self.setup_bookmark(user=self.user, is_archived=True)
# capture number of queries # capture number of queries
@ -37,7 +40,7 @@ class BookmarkArchivedViewPerformanceTestCase(
# add more bookmarks # add more bookmarks
num_additional_bookmarks = 10 num_additional_bookmarks = 10
for index in range(num_additional_bookmarks): for _ in range(num_additional_bookmarks):
self.setup_bookmark(user=self.user, is_archived=True) self.setup_bookmark(user=self.user, is_archived=True)
# assert num queries doesn't increase # assert num queries doesn't increase

View file

@ -1,10 +1,10 @@
from django.contrib.auth.models import User from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from django.test import TransactionTestCase from django.test import TransactionTestCase
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
from django.urls import reverse from django.urls import reverse
from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from bookmarks.models import GlobalSettings
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
@ -18,9 +18,12 @@ class BookmarkIndexViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryM
return connections[DEFAULT_DB_ALIAS] return connections[DEFAULT_DB_ALIAS]
def test_should_not_increase_number_of_queries_per_bookmark(self): def test_should_not_increase_number_of_queries_per_bookmark(self):
# create global settings
GlobalSettings.get()
# create initial bookmarks # create initial bookmarks
num_initial_bookmarks = 10 num_initial_bookmarks = 10
for index in range(num_initial_bookmarks): for _ in range(num_initial_bookmarks):
self.setup_bookmark(user=self.user) self.setup_bookmark(user=self.user)
# capture number of queries # capture number of queries
@ -35,7 +38,7 @@ class BookmarkIndexViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryM
# add more bookmarks # add more bookmarks
num_additional_bookmarks = 10 num_additional_bookmarks = 10
for index in range(num_additional_bookmarks): for _ in range(num_additional_bookmarks):
self.setup_bookmark(user=self.user) self.setup_bookmark(user=self.user)
# assert num queries doesn't increase # assert num queries doesn't increase

View file

@ -1,10 +1,10 @@
from django.contrib.auth.models import User from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from django.test import TransactionTestCase from django.test import TransactionTestCase
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
from django.urls import reverse from django.urls import reverse
from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from bookmarks.models import GlobalSettings
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
@ -18,9 +18,12 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
return connections[DEFAULT_DB_ALIAS] return connections[DEFAULT_DB_ALIAS]
def test_should_not_increase_number_of_queries_per_bookmark(self): def test_should_not_increase_number_of_queries_per_bookmark(self):
# create global settings
GlobalSettings.get()
# create initial users and bookmarks # create initial users and bookmarks
num_initial_bookmarks = 10 num_initial_bookmarks = 10
for index in range(num_initial_bookmarks): for _ in range(num_initial_bookmarks):
user = self.setup_user(enable_sharing=True) user = self.setup_user(enable_sharing=True)
self.setup_bookmark(user=user, shared=True) self.setup_bookmark(user=user, shared=True)
@ -36,7 +39,7 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
# add more users and bookmarks # add more users and bookmarks
num_additional_bookmarks = 10 num_additional_bookmarks = 10
for index in range(num_additional_bookmarks): for _ in range(num_additional_bookmarks):
user = self.setup_user(enable_sharing=True) user = self.setup_user(enable_sharing=True)
self.setup_bookmark(user=user, shared=True) self.setup_bookmark(user=user, shared=True)

View file

@ -5,6 +5,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from bookmarks.models import GlobalSettings
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
@ -16,13 +17,16 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
)[0] )[0]
self.client.credentials(HTTP_AUTHORIZATION="Token " + self.api_token.key) self.client.credentials(HTTP_AUTHORIZATION="Token " + self.api_token.key)
# create global settings
GlobalSettings.get()
def get_connection(self): def get_connection(self):
return connections[DEFAULT_DB_ALIAS] return connections[DEFAULT_DB_ALIAS]
def test_list_bookmarks_max_queries(self): def test_list_bookmarks_max_queries(self):
# set up some bookmarks with associated tags # set up some bookmarks with associated tags
num_initial_bookmarks = 10 num_initial_bookmarks = 10
for index in range(num_initial_bookmarks): for _ in range(num_initial_bookmarks):
self.setup_bookmark(tags=[self.setup_tag()]) self.setup_bookmark(tags=[self.setup_tag()])
# capture number of queries # capture number of queries
@ -40,7 +44,7 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_list_archived_bookmarks_max_queries(self): def test_list_archived_bookmarks_max_queries(self):
# set up some bookmarks with associated tags # set up some bookmarks with associated tags
num_initial_bookmarks = 10 num_initial_bookmarks = 10
for index in range(num_initial_bookmarks): for _ in range(num_initial_bookmarks):
self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]) self.setup_bookmark(is_archived=True, tags=[self.setup_tag()])
# capture number of queries # capture number of queries
@ -59,7 +63,7 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
# set up some bookmarks with associated tags # set up some bookmarks with associated tags
share_user = self.setup_user(enable_sharing=True) share_user = self.setup_user(enable_sharing=True)
num_initial_bookmarks = 10 num_initial_bookmarks = 10
for index in range(num_initial_bookmarks): for _ in range(num_initial_bookmarks):
self.setup_bookmark(user=share_user, shared=True, tags=[self.setup_tag()]) self.setup_bookmark(user=share_user, shared=True, tags=[self.setup_tag()])
# capture number of queries # capture number of queries

View file

@ -9,7 +9,7 @@ from django.test import TestCase, RequestFactory
from django.urls import reverse from django.urls import reverse
from django.utils import timezone, formats from django.utils import timezone, formats
from bookmarks.middlewares import UserProfileMiddleware from bookmarks.middlewares import LinkdingMiddleware
from bookmarks.models import Bookmark, UserProfile, User from bookmarks.models import Bookmark, UserProfile, User
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
from bookmarks.views.partials import contexts from bookmarks.views.partials import contexts
@ -74,6 +74,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
f""" f"""
<a ld-fetch="{details_modal_url}?return_url={return_url}" <a ld-fetch="{details_modal_url}?return_url={return_url}"
ld-on="click" ld-target="body|append" ld-on="click" ld-target="body|append"
data-turbo-prefetch="false"
href="{details_url}">View</a> href="{details_url}">View</a>
""", """,
html, html,
@ -270,7 +271,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) request = rf.get(url)
request.user = user or self.get_or_create_test_user() request.user = user or self.get_or_create_test_user()
middleware = UserProfileMiddleware(lambda r: HttpResponse()) middleware = LinkdingMiddleware(lambda r: HttpResponse())
middleware(request) middleware(request)
bookmark_list_context = context_type(request) bookmark_list_context = context_type(request)

View file

@ -4,7 +4,7 @@ from django.test import TestCase
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
from django.urls import reverse from django.urls import reverse
from bookmarks.models import FeedToken from bookmarks.models import FeedToken, GlobalSettings
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
@ -15,13 +15,16 @@ class FeedsPerformanceTestCase(TestCase, BookmarkFactoryMixin):
self.client.force_login(user) self.client.force_login(user)
self.token = FeedToken.objects.get_or_create(user=user)[0] self.token = FeedToken.objects.get_or_create(user=user)[0]
# create global settings
GlobalSettings.get()
def get_connection(self): def get_connection(self):
return connections[DEFAULT_DB_ALIAS] return connections[DEFAULT_DB_ALIAS]
def test_all_max_queries(self): def test_all_max_queries(self):
# set up some bookmarks with associated tags # set up some bookmarks with associated tags
num_initial_bookmarks = 10 num_initial_bookmarks = 10
for index in range(num_initial_bookmarks): for _ in range(num_initial_bookmarks):
self.setup_bookmark(tags=[self.setup_tag()]) self.setup_bookmark(tags=[self.setup_tag()])
# capture number of queries # capture number of queries

View file

@ -1,16 +1,17 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from bookmarks.models import GlobalSettings
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
class NavMenuTestCase(TestCase, BookmarkFactoryMixin): class LayoutTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self) -> None: def setUp(self) -> None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)
def test_should_respect_share_profile_setting(self): def test_nav_menu_should_respect_share_profile_setting(self):
self.user.profile.enable_sharing = False self.user.profile.enable_sharing = False
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse("bookmarks:index")) response = self.client.get(reverse("bookmarks:index"))
@ -36,3 +37,29 @@ class NavMenuTestCase(TestCase, BookmarkFactoryMixin):
html, html,
count=2, count=2,
) )
def test_metadata_should_respect_prefetch_links_setting(self):
settings = GlobalSettings.get()
settings.enable_link_prefetch = False
settings.save()
response = self.client.get(reverse("bookmarks:index"))
html = response.content.decode()
self.assertInHTML(
'<meta name="turbo-prefetch" content="false">',
html,
count=1,
)
settings.enable_link_prefetch = True
settings.save()
response = self.client.get(reverse("bookmarks:index"))
html = response.content.decode()
self.assertInHTML(
'<meta name="turbo-prefetch" content="false">',
html,
count=0,
)

View file

@ -6,7 +6,7 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
from bookmarks.middlewares import standard_profile from bookmarks.middlewares import standard_profile
class UserProfileMiddlewareTestCase(TestCase, BookmarkFactoryMixin): class LinkdingMiddlewareTestCase(TestCase, BookmarkFactoryMixin):
def test_unauthenticated_user_should_use_standard_profile_by_default(self): def test_unauthenticated_user_should_use_standard_profile_by_default(self):
response = self.client.get(reverse("login")) response = self.client.get(reverse("login"))

View file

@ -79,6 +79,13 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
reverse("login") + "?next=" + reverse("bookmarks:settings.general"), reverse("login") + "?next=" + reverse("bookmarks:settings.general"),
) )
response = self.client.get(reverse("bookmarks:settings.update"), follow=True)
self.assertRedirects(
response,
reverse("login") + "?next=" + reverse("bookmarks:settings.update"),
)
def test_update_profile(self): def test_update_profile(self):
form_data = { form_data = {
"update_profile": "", "update_profile": "",
@ -105,7 +112,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"custom_css": "body { background-color: #000; }", "custom_css": "body { background-color: #000; }",
"auto_tagging_rules": "example.com tag", "auto_tagging_rules": "example.com tag",
} }
response = self.client.post(reverse("bookmarks:settings.general"), form_data) response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
)
html = response.content.decode() html = response.content.decode()
self.user.profile.refresh_from_db() self.user.profile.refresh_from_db()
@ -179,7 +188,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
form_data = { form_data = {
"theme": UserProfile.THEME_DARK, "theme": UserProfile.THEME_DARK,
} }
response = self.client.post(reverse("bookmarks:settings.general"), form_data) response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
)
html = response.content.decode() html = response.content.decode()
self.user.profile.refresh_from_db() self.user.profile.refresh_from_db()
@ -199,14 +210,14 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"enable_favicons": True, "enable_favicons": True,
} }
) )
self.client.post(reverse("bookmarks:settings.general"), form_data) self.client.post(reverse("bookmarks:settings.update"), form_data)
mock_schedule_bookmarks_without_favicons.assert_called_once_with(self.user) mock_schedule_bookmarks_without_favicons.assert_called_once_with(self.user)
# No update scheduled if favicons are already enabled # No update scheduled if favicons are already enabled
mock_schedule_bookmarks_without_favicons.reset_mock() mock_schedule_bookmarks_without_favicons.reset_mock()
self.client.post(reverse("bookmarks:settings.general"), form_data) self.client.post(reverse("bookmarks:settings.update"), form_data)
mock_schedule_bookmarks_without_favicons.assert_not_called() mock_schedule_bookmarks_without_favicons.assert_not_called()
@ -217,7 +228,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
} }
) )
self.client.post(reverse("bookmarks:settings.general"), form_data) self.client.post(reverse("bookmarks:settings.update"), form_data)
mock_schedule_bookmarks_without_favicons.assert_not_called() mock_schedule_bookmarks_without_favicons.assert_not_called()
@ -229,7 +240,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"refresh_favicons": "", "refresh_favicons": "",
} }
response = self.client.post( response = self.client.post(
reverse("bookmarks:settings.general"), form_data reverse("bookmarks:settings.update"), form_data, follow=True
) )
html = response.content.decode() html = response.content.decode()
@ -243,9 +254,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
tasks, "schedule_refresh_favicons" tasks, "schedule_refresh_favicons"
) as mock_schedule_refresh_favicons: ) as mock_schedule_refresh_favicons:
form_data = {} form_data = {}
response = self.client.post( response = self.client.post(reverse("bookmarks:settings.update"), form_data)
reverse("bookmarks:settings.general"), form_data
)
html = response.content.decode() html = response.content.decode()
mock_schedule_refresh_favicons.assert_not_called() mock_schedule_refresh_favicons.assert_not_called()
@ -315,14 +324,14 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"enable_preview_images": True, "enable_preview_images": True,
} }
) )
self.client.post(reverse("bookmarks:settings.general"), form_data) self.client.post(reverse("bookmarks:settings.update"), form_data)
mock_schedule_bookmarks_without_previews.assert_called_once_with(self.user) mock_schedule_bookmarks_without_previews.assert_called_once_with(self.user)
# No update scheduled if favicons are already enabled # No update scheduled if favicons are already enabled
mock_schedule_bookmarks_without_previews.reset_mock() mock_schedule_bookmarks_without_previews.reset_mock()
self.client.post(reverse("bookmarks:settings.general"), form_data) self.client.post(reverse("bookmarks:settings.update"), form_data)
mock_schedule_bookmarks_without_previews.assert_not_called() mock_schedule_bookmarks_without_previews.assert_not_called()
@ -333,7 +342,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
} }
) )
self.client.post(reverse("bookmarks:settings.general"), form_data) self.client.post(reverse("bookmarks:settings.update"), form_data)
mock_schedule_bookmarks_without_previews.assert_not_called() mock_schedule_bookmarks_without_previews.assert_not_called()
@ -422,10 +431,11 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"create_missing_html_snapshots": "", "create_missing_html_snapshots": "",
} }
response = self.client.post( response = self.client.post(
reverse("bookmarks:settings.general"), form_data reverse("bookmarks:settings.update"), form_data, follow=True
) )
html = response.content.decode() html = response.content.decode()
self.assertEqual(response.status_code, 200)
mock_create_missing_html_snapshots.assert_called_once() mock_create_missing_html_snapshots.assert_called_once()
self.assertSuccessMessage( self.assertSuccessMessage(
html, "Queued 5 missing snapshots. This may take a while..." html, "Queued 5 missing snapshots. This may take a while..."
@ -441,10 +451,11 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"create_missing_html_snapshots": "", "create_missing_html_snapshots": "",
} }
response = self.client.post( response = self.client.post(
reverse("bookmarks:settings.general"), form_data reverse("bookmarks:settings.update"), form_data, follow=True
) )
html = response.content.decode() html = response.content.decode()
self.assertEqual(response.status_code, 200)
mock_create_missing_html_snapshots.assert_called_once() mock_create_missing_html_snapshots.assert_called_once()
self.assertSuccessMessage(html, "No missing snapshots found.") self.assertSuccessMessage(html, "No missing snapshots found.")
@ -457,10 +468,11 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
mock_create_missing_html_snapshots.return_value = 5 mock_create_missing_html_snapshots.return_value = 5
form_data = {} form_data = {}
response = self.client.post( response = self.client.post(
reverse("bookmarks:settings.general"), form_data reverse("bookmarks:settings.update"), form_data, follow=True
) )
html = response.content.decode() html = response.content.decode()
self.assertEqual(response.status_code, 200)
mock_create_missing_html_snapshots.assert_not_called() mock_create_missing_html_snapshots.assert_not_called()
self.assertSuccessMessage( self.assertSuccessMessage(
html, "Queued 5 missing snapshots. This may take a while...", count=0 html, "Queued 5 missing snapshots. This may take a while...", count=0
@ -477,7 +489,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS, "landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS,
"guest_profile_user": selectable_user.id, "guest_profile_user": selectable_user.id,
} }
response = self.client.post(reverse("bookmarks:settings.general"), form_data) response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertSuccessMessage(response.content.decode(), "Global settings updated") self.assertSuccessMessage(response.content.decode(), "Global settings updated")
@ -491,7 +505,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"landing_page": GlobalSettings.LANDING_PAGE_LOGIN, "landing_page": GlobalSettings.LANDING_PAGE_LOGIN,
"guest_profile_user": "", "guest_profile_user": "",
} }
response = self.client.post(reverse("bookmarks:settings.general"), form_data) response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertSuccessMessage(response.content.decode(), "Global settings updated") self.assertSuccessMessage(response.content.decode(), "Global settings updated")
@ -509,7 +525,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
form_data = { form_data = {
"landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS, "landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS,
} }
response = self.client.post(reverse("bookmarks:settings.general"), form_data) response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertSuccessMessage( self.assertSuccessMessage(
response.content.decode(), "Global settings updated", count=0 response.content.decode(), "Global settings updated", count=0
@ -520,7 +538,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"update_global_settings": "", "update_global_settings": "",
"landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS, "landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS,
} }
response = self.client.post(reverse("bookmarks:settings.general"), form_data) response = self.client.post(reverse("bookmarks:settings.update"), form_data)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_global_settings_only_visible_for_superuser(self): def test_global_settings_only_visible_for_superuser(self):

View file

@ -68,17 +68,18 @@ class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
token = FeedToken.objects.first() token = FeedToken.objects.first()
self.assertInHTML( self.assertInHTML(
f'<a href="http://testserver/feeds/{token.key}/all">All bookmarks</a>', html f'<a target="_blank" href="http://testserver/feeds/{token.key}/all">All bookmarks</a>',
)
self.assertInHTML(
f'<a href="http://testserver/feeds/{token.key}/unread">Unread bookmarks</a>',
html, html,
) )
self.assertInHTML( self.assertInHTML(
f'<a href="http://testserver/feeds/{token.key}/shared">Shared bookmarks</a>', f'<a target="_blank" href="http://testserver/feeds/{token.key}/unread">Unread bookmarks</a>',
html, html,
) )
self.assertInHTML( self.assertInHTML(
f'<a href="http://testserver/feeds/shared">Public shared bookmarks</a>', f'<a target="_blank" href="http://testserver/feeds/{token.key}/shared">Shared bookmarks</a>',
html,
)
self.assertInHTML(
'<a target="_blank" href="http://testserver/feeds/shared">Public shared bookmarks</a>',
html, html,
) )

View file

@ -5,7 +5,7 @@ from django.http import HttpResponse
from django.template import Template, RequestContext from django.template import Template, RequestContext
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from bookmarks.middlewares import UserProfileMiddleware from bookmarks.middlewares import LinkdingMiddleware
from bookmarks.models import UserProfile from bookmarks.models import UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
from bookmarks.views.partials import contexts from bookmarks.views.partials import contexts
@ -21,7 +21,7 @@ class TagCloudTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) request = rf.get(url)
request.user = user or self.get_or_create_test_user() request.user = user or self.get_or_create_test_user()
middleware = UserProfileMiddleware(lambda r: HttpResponse()) middleware = LinkdingMiddleware(lambda r: HttpResponse())
middleware(request) middleware(request)
tag_cloud_context = context_type(request) tag_cloud_context = context_type(request)

View file

@ -106,6 +106,7 @@ urlpatterns = [
# Settings # Settings
path("settings", views.settings.general, name="settings.index"), path("settings", views.settings.general, name="settings.index"),
path("settings/general", views.settings.general, name="settings.general"), path("settings/general", views.settings.general, name="settings.general"),
path("settings/update", views.settings.update, name="settings.update"),
path( path(
"settings/integrations", "settings/integrations",
views.settings.integrations, views.settings.integrations,

View file

@ -189,6 +189,7 @@ def convert_tag_string(tag_string: str):
@login_required @login_required
def new(request): def new(request):
status = 200
initial_url = request.GET.get("url") initial_url = request.GET.get("url")
initial_title = request.GET.get("title") initial_title = request.GET.get("title")
initial_description = request.GET.get("description") initial_description = request.GET.get("description")
@ -207,6 +208,8 @@ def new(request):
return HttpResponseRedirect(reverse("bookmarks:close")) return HttpResponseRedirect(reverse("bookmarks:close"))
else: else:
return HttpResponseRedirect(reverse("bookmarks:index")) return HttpResponseRedirect(reverse("bookmarks:index"))
else:
status = 422
else: else:
form = BookmarkForm() form = BookmarkForm()
if initial_url: if initial_url:
@ -228,7 +231,7 @@ def new(request):
"return_url": reverse("bookmarks:index"), "return_url": reverse("bookmarks:index"),
} }
return render(request, "bookmarks/new.html", context) return render(request, "bookmarks/new.html", context, status=status)
@login_required @login_required

View file

@ -7,7 +7,7 @@ from bookmarks.models import GlobalSettings
def root(request): def root(request):
# Redirect unauthenticated users to the configured landing page # Redirect unauthenticated users to the configured landing page
if not request.user.is_authenticated: if not request.user.is_authenticated:
settings = GlobalSettings.get() settings = request.global_settings
if settings.landing_page == GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS: if settings.landing_page == GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS:
return HttpResponseRedirect(reverse("bookmarks:shared")) return HttpResponseRedirect(reverse("bookmarks:shared"))

View file

@ -29,41 +29,19 @@ logger = logging.getLogger(__name__)
@login_required @login_required
def general(request): def general(request):
profile_form = None
global_settings_form = None
enable_refresh_favicons = django_settings.LD_ENABLE_REFRESH_FAVICONS enable_refresh_favicons = django_settings.LD_ENABLE_REFRESH_FAVICONS
has_snapshot_support = django_settings.LD_ENABLE_SNAPSHOTS has_snapshot_support = django_settings.LD_ENABLE_SNAPSHOTS
success_message = _find_message_with_tag( success_message = _find_message_with_tag(
messages.get_messages(request), "bookmark_import_success" messages.get_messages(request), "settings_success_message"
) )
error_message = _find_message_with_tag( error_message = _find_message_with_tag(
messages.get_messages(request), "bookmark_import_errors" messages.get_messages(request), "settings_error_message"
) )
version_info = get_version_info(get_ttl_hash()) version_info = get_version_info(get_ttl_hash())
if request.method == "POST": profile_form = UserProfileForm(instance=request.user_profile)
if "update_profile" in request.POST: global_settings_form = None
profile_form = update_profile(request) if request.user.is_superuser:
success_message = "Profile updated"
if "update_global_settings" in request.POST:
global_settings_form = update_global_settings(request)
success_message = "Global settings updated"
if "refresh_favicons" in request.POST:
tasks.schedule_refresh_favicons(request.user)
success_message = "Scheduled favicon update. This may take a while..."
if "create_missing_html_snapshots" in request.POST:
count = tasks.create_missing_html_snapshots(request.user)
if count > 0:
success_message = (
f"Queued {count} missing snapshots. This may take a while..."
)
else:
success_message = "No missing snapshots found."
if not profile_form:
profile_form = UserProfileForm(instance=request.user_profile)
if request.user.is_superuser and not global_settings_form:
global_settings_form = GlobalSettingsForm(instance=GlobalSettings.get()) global_settings_form = GlobalSettingsForm(instance=GlobalSettings.get())
return render( return render(
@ -81,6 +59,40 @@ def general(request):
) )
@login_required
def update(request):
if request.method == "POST":
if "update_profile" in request.POST:
update_profile(request)
messages.success(request, "Profile updated", "settings_success_message")
if "update_global_settings" in request.POST:
update_global_settings(request)
messages.success(
request, "Global settings updated", "settings_success_message"
)
if "refresh_favicons" in request.POST:
tasks.schedule_refresh_favicons(request.user)
messages.success(
request,
"Scheduled favicon update. This may take a while...",
"settings_success_message",
)
if "create_missing_html_snapshots" in request.POST:
count = tasks.create_missing_html_snapshots(request.user)
if count > 0:
messages.success(
request,
f"Queued {count} missing snapshots. This may take a while...",
"settings_success_message",
)
else:
messages.success(
request, "No missing snapshots found.", "settings_success_message"
)
return HttpResponseRedirect(reverse("bookmarks:settings.general"))
def update_profile(request): def update_profile(request):
user = request.user user = request.user
profile = user.profile profile = user.profile
@ -178,7 +190,7 @@ def bookmark_import(request):
if import_file is None: if import_file is None:
messages.error( messages.error(
request, "Please select a file to import.", "bookmark_import_errors" request, "Please select a file to import.", "settings_error_message"
) )
return HttpResponseRedirect(reverse("bookmarks:settings.general")) return HttpResponseRedirect(reverse("bookmarks:settings.general"))
@ -186,21 +198,20 @@ def bookmark_import(request):
content = import_file.read().decode() content = import_file.read().decode()
result = importer.import_netscape_html(content, request.user, import_options) result = importer.import_netscape_html(content, request.user, import_options)
success_msg = str(result.success) + " bookmarks were successfully imported." success_msg = str(result.success) + " bookmarks were successfully imported."
messages.success(request, success_msg, "bookmark_import_success") messages.success(request, success_msg, "settings_success_message")
if result.failed > 0: if result.failed > 0:
err_msg = ( err_msg = (
str(result.failed) str(result.failed)
+ " bookmarks could not be imported. Please check the logs for more details." + " bookmarks could not be imported. Please check the logs for more details."
) )
messages.error(request, err_msg, "bookmark_import_errors") messages.error(request, err_msg, "settings_error_message")
except: except:
logging.exception("Unexpected error during bookmark import") logging.exception("Unexpected error during bookmark import")
messages.error( messages.error(
request, request,
"An error occurred during bookmark import.", "An error occurred during bookmark import.",
"bookmark_import_errors", "settings_error_message",
) )
pass
return HttpResponseRedirect(reverse("bookmarks:settings.general")) return HttpResponseRedirect(reverse("bookmarks:settings.general"))

13
package-lock.json generated
View file

@ -1,14 +1,15 @@
{ {
"name": "linkding", "name": "linkding",
"version": "1.31.1", "version": "1.32.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "linkding", "name": "linkding",
"version": "1.31.1", "version": "1.32.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hotwired/turbo": "^8.0.6",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/wasm-node": "^4.13.0", "@rollup/wasm-node": "^4.13.0",
@ -79,6 +80,14 @@
"postcss-selector-parser": "^6.1.0" "postcss-selector-parser": "^6.1.0"
} }
}, },
"node_modules/@hotwired/turbo": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.6.tgz",
"integrity": "sha512-mwZRfwcJ4yatUnW5tcCY9NDvo0kjuuLQF/y8pXigHhS+c/JY/ccNluVyuERR9Sraqx0qdpenkO3pNeSWz1mE3w==",
"engines": {
"node": ">= 14"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",

View file

@ -22,6 +22,7 @@
}, },
"homepage": "https://github.com/sissbruecker/linkding#readme", "homepage": "https://github.com/sissbruecker/linkding#readme",
"dependencies": { "dependencies": {
"@hotwired/turbo": "^8.0.6",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/wasm-node": "^4.13.0", "@rollup/wasm-node": "^4.13.0",

View file

@ -52,7 +52,7 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"bookmarks.middlewares.UserProfileMiddleware", "bookmarks.middlewares.LinkdingMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",