diff --git a/bookmarks/frontend/behaviors/index.js b/bookmarks/frontend/behaviors/index.js index 0ddbde38..3368f7e7 100644 --- a/bookmarks/frontend/behaviors/index.js +++ b/bookmarks/frontend/behaviors/index.js @@ -16,9 +16,13 @@ const mutationObserver = new MutationObserver((mutations) => { }); }); -mutationObserver.observe(document.body, { - childList: true, - subtree: true, +window.addEventListener("turbo:load", () => { + mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }); + + applyBehaviors(document.body); }); export class Behavior { diff --git a/bookmarks/frontend/index.js b/bookmarks/frontend/index.js index af81505c..4e35403e 100644 --- a/bookmarks/frontend/index.js +++ b/bookmarks/frontend/index.js @@ -1,3 +1,4 @@ +import "@hotwired/turbo"; import "./behaviors/bookmark-page"; import "./behaviors/bulk-edit"; import "./behaviors/confirm-button"; diff --git a/bookmarks/middlewares.py b/bookmarks/middlewares.py index af7ff12a..7469d2e6 100644 --- a/bookmarks/middlewares.py +++ b/bookmarks/middlewares.py @@ -8,28 +8,33 @@ class CustomRemoteUserMiddleware(RemoteUserMiddleware): header = settings.LD_AUTH_PROXY_USERNAME_HEADER +default_global_settings = GlobalSettings() + standard_profile = UserProfile() standard_profile.enable_favicons = True -class UserProfileMiddleware: +class LinkdingMiddleware: def __init__(self, get_response): self.get_response = get_response 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: request.user_profile = request.user.profile else: # check if a custom profile for guests exists, otherwise use standard profile - guest_profile = None - try: - global_settings = GlobalSettings.get() - if global_settings.guest_profile_user: - guest_profile = global_settings.guest_profile_user.profile - except: - pass - - request.user_profile = guest_profile or standard_profile + if global_settings.guest_profile_user: + request.user_profile = global_settings.guest_profile_user.profile + else: + request.user_profile = standard_profile response = self.get_response(request) diff --git a/bookmarks/migrations/0039_globalsettings_enable_link_prefetch.py b/bookmarks/migrations/0039_globalsettings_enable_link_prefetch.py new file mode 100644 index 00000000..b18ec957 --- /dev/null +++ b/bookmarks/migrations/0039_globalsettings_enable_link_prefetch.py @@ -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), + ), + ] diff --git a/bookmarks/models.py b/bookmarks/models.py index a8d5a81e..aa8990d0 100644 --- a/bookmarks/models.py +++ b/bookmarks/models.py @@ -514,6 +514,7 @@ class GlobalSettings(models.Model): guest_profile_user = models.ForeignKey( get_user_model(), on_delete=models.SET_NULL, null=True, blank=True ) + enable_link_prefetch = models.BooleanField(default=False, null=False) @classmethod def get(cls): @@ -532,7 +533,7 @@ def save(self, *args, **kwargs): class GlobalSettingsForm(forms.ModelForm): class Meta: model = GlobalSettings - fields = ["landing_page", "guest_profile_user"] + fields = ["landing_page", "guest_profile_user", "enable_link_prefetch"] def __init__(self, *args, **kwargs): super(GlobalSettingsForm, self).__init__(*args, **kwargs) diff --git a/bookmarks/styles/components.css b/bookmarks/styles/components.css index f485f0e3..9df1bdfe 100644 --- a/bookmarks/styles/components.css +++ b/bookmarks/styles/components.css @@ -57,4 +57,9 @@ span.confirmation { .divider { border-bottom: solid 1px var(--secondary-border-color); margin: var(--unit-5) 0; -} \ No newline at end of file +} + +/* Turbo progress bar */ +.turbo-progress-bar { + background-color: var(--primary-color); +} diff --git a/bookmarks/templates/bookmarks/bookmark_list.html b/bookmarks/templates/bookmarks/bookmark_list.html index ae04219d..71d0adae 100644 --- a/bookmarks/templates/bookmarks/bookmark_list.html +++ b/bookmarks/templates/bookmarks/bookmark_list.html @@ -26,7 +26,7 @@ {% if bookmark_list.show_url %}
+ class="url-display"> {{ bookmark_item.url }}
@@ -66,9 +66,9 @@ {% if bookmark_item.display_date %} {% if bookmark_item.web_archive_snapshot_url %} + title="Show snapshot on the Internet Archive Wayback Machine" + target="{{ bookmark_list.link_target }}" + rel="noopener"> {{ bookmark_item.display_date }} {% else %} @@ -79,8 +79,9 @@ {# View link is visible for both owned and shared bookmarks #} {% if bookmark_list.show_view_action %} View + ld-on="click" ld-target="body|append" + data-turbo-prefetch="false" + href="{% url 'bookmarks:details' bookmark_item.id %}">View {% endif %} {% if bookmark_item.is_editable %} {# Bookmark owner actions #} diff --git a/bookmarks/templates/bookmarks/layout.html b/bookmarks/templates/bookmarks/layout.html index 03ff4e30..c05e2216 100644 --- a/bookmarks/templates/bookmarks/layout.html +++ b/bookmarks/templates/bookmarks/layout.html @@ -35,6 +35,11 @@ {% if request.user_profile.custom_css %} {% endif %} + + {% if not request.global_settings.enable_link_prefetch %} + + {% endif %} + @@ -129,6 +134,5 @@

LINKDING

{% block content %} {% endblock %} - diff --git a/bookmarks/templates/bookmarks/search.html b/bookmarks/templates/bookmarks/search.html index 06d9f7b8..c9e5868a 100644 --- a/bookmarks/templates/bookmarks/search.html +++ b/bookmarks/templates/bookmarks/search.html @@ -77,7 +77,7 @@ {# Replace search input with auto-complete component #} \ No newline at end of file diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html index aeb7c001..ec01aea8 100644 --- a/bookmarks/templates/settings/general.html +++ b/bookmarks/templates/settings/general.html @@ -19,7 +19,7 @@

Profile

Change password

-
+ {% csrf_token %}
@@ -247,7 +247,7 @@

Profile

{% if global_settings_form %}

Global settings

- + {% csrf_token %}
@@ -266,6 +266,16 @@

Global settings

a dedicated user for this purpose. By default, a standard profile with fixed settings is used.
+
+ +
+ 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. +
+
@@ -306,7 +316,7 @@

Import

Export

Export all bookmarks in Netscape HTML format.

- Download (.html) + Download (.html) {% if export_error %}

@@ -344,35 +354,37 @@

About

{% endblock %} diff --git a/bookmarks/templates/settings/integrations.html b/bookmarks/templates/settings/integrations.html index 55bc8365..0c925bfc 100644 --- a/bookmarks/templates/settings/integrations.html +++ b/bookmarks/templates/settings/integrations.html @@ -52,10 +52,10 @@

REST API

RSS Feeds

The following URLs provide RSS feeds for your bookmarks:

@@ -80,7 +80,7 @@

RSS Feeds

credential. 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 admin panel. + target="_blank" href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel. After deleting the feed token, new URLs will be generated when you reload this settings page.

diff --git a/bookmarks/tests/test_bookmark_archived_view_performance.py b/bookmarks/tests/test_bookmark_archived_view_performance.py index 81042b6e..ea1f0af3 100644 --- a/bookmarks/tests/test_bookmark_archived_view_performance.py +++ b/bookmarks/tests/test_bookmark_archived_view_performance.py @@ -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.utils import CaptureQueriesContext 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 @@ -20,9 +20,12 @@ def get_connection(self): return connections[DEFAULT_DB_ALIAS] def test_should_not_increase_number_of_queries_per_bookmark(self): + # create global settings + GlobalSettings.get() + # create initial bookmarks 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) # capture number of queries @@ -37,7 +40,7 @@ def test_should_not_increase_number_of_queries_per_bookmark(self): # add more bookmarks 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) # assert num queries doesn't increase diff --git a/bookmarks/tests/test_bookmark_index_view_performance.py b/bookmarks/tests/test_bookmark_index_view_performance.py index ac508951..8f84469a 100644 --- a/bookmarks/tests/test_bookmark_index_view_performance.py +++ b/bookmarks/tests/test_bookmark_index_view_performance.py @@ -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.utils import CaptureQueriesContext 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 @@ -18,9 +18,12 @@ def get_connection(self): return connections[DEFAULT_DB_ALIAS] def test_should_not_increase_number_of_queries_per_bookmark(self): + # create global settings + GlobalSettings.get() + # create initial bookmarks num_initial_bookmarks = 10 - for index in range(num_initial_bookmarks): + for _ in range(num_initial_bookmarks): self.setup_bookmark(user=self.user) # capture number of queries @@ -35,7 +38,7 @@ def test_should_not_increase_number_of_queries_per_bookmark(self): # add more bookmarks num_additional_bookmarks = 10 - for index in range(num_additional_bookmarks): + for _ in range(num_additional_bookmarks): self.setup_bookmark(user=self.user) # assert num queries doesn't increase diff --git a/bookmarks/tests/test_bookmark_shared_view_performance.py b/bookmarks/tests/test_bookmark_shared_view_performance.py index 05ce95f2..872340d4 100644 --- a/bookmarks/tests/test_bookmark_shared_view_performance.py +++ b/bookmarks/tests/test_bookmark_shared_view_performance.py @@ -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.utils import CaptureQueriesContext 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 @@ -18,9 +18,12 @@ def get_connection(self): return connections[DEFAULT_DB_ALIAS] def test_should_not_increase_number_of_queries_per_bookmark(self): + # create global settings + GlobalSettings.get() + # create initial users and bookmarks num_initial_bookmarks = 10 - for index in range(num_initial_bookmarks): + for _ in range(num_initial_bookmarks): user = self.setup_user(enable_sharing=True) self.setup_bookmark(user=user, shared=True) @@ -36,7 +39,7 @@ def test_should_not_increase_number_of_queries_per_bookmark(self): # add more users and bookmarks num_additional_bookmarks = 10 - for index in range(num_additional_bookmarks): + for _ in range(num_additional_bookmarks): user = self.setup_user(enable_sharing=True) self.setup_bookmark(user=user, shared=True) diff --git a/bookmarks/tests/test_bookmarks_api_performance.py b/bookmarks/tests/test_bookmarks_api_performance.py index 0c8d26ec..61572d35 100644 --- a/bookmarks/tests/test_bookmarks_api_performance.py +++ b/bookmarks/tests/test_bookmarks_api_performance.py @@ -5,6 +5,7 @@ from rest_framework import status from rest_framework.authtoken.models import Token +from bookmarks.models import GlobalSettings from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin @@ -16,13 +17,16 @@ def setUp(self) -> None: )[0] self.client.credentials(HTTP_AUTHORIZATION="Token " + self.api_token.key) + # create global settings + GlobalSettings.get() + def get_connection(self): return connections[DEFAULT_DB_ALIAS] def test_list_bookmarks_max_queries(self): # set up some bookmarks with associated tags num_initial_bookmarks = 10 - for index in range(num_initial_bookmarks): + for _ in range(num_initial_bookmarks): self.setup_bookmark(tags=[self.setup_tag()]) # capture number of queries @@ -40,7 +44,7 @@ def test_list_bookmarks_max_queries(self): def test_list_archived_bookmarks_max_queries(self): # set up some bookmarks with associated tags 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()]) # capture number of queries @@ -59,7 +63,7 @@ def test_list_shared_bookmarks_max_queries(self): # set up some bookmarks with associated tags share_user = self.setup_user(enable_sharing=True) 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()]) # capture number of queries diff --git a/bookmarks/tests/test_bookmarks_list_template.py b/bookmarks/tests/test_bookmarks_list_template.py index 86d1c7e6..c86313e2 100644 --- a/bookmarks/tests/test_bookmarks_list_template.py +++ b/bookmarks/tests/test_bookmarks_list_template.py @@ -9,7 +9,7 @@ from django.urls import reverse 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.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin from bookmarks.views.partials import contexts @@ -74,6 +74,7 @@ def assertViewLinkCount( f""" View """, html, @@ -270,7 +271,7 @@ def render_template( rf = RequestFactory() request = rf.get(url) request.user = user or self.get_or_create_test_user() - middleware = UserProfileMiddleware(lambda r: HttpResponse()) + middleware = LinkdingMiddleware(lambda r: HttpResponse()) middleware(request) bookmark_list_context = context_type(request) diff --git a/bookmarks/tests/test_feeds_performance.py b/bookmarks/tests/test_feeds_performance.py index 97941a9d..3aebebc1 100644 --- a/bookmarks/tests/test_feeds_performance.py +++ b/bookmarks/tests/test_feeds_performance.py @@ -4,7 +4,7 @@ from django.test.utils import CaptureQueriesContext from django.urls import reverse -from bookmarks.models import FeedToken +from bookmarks.models import FeedToken, GlobalSettings from bookmarks.tests.helpers import BookmarkFactoryMixin @@ -15,13 +15,16 @@ def setUp(self) -> None: self.client.force_login(user) self.token = FeedToken.objects.get_or_create(user=user)[0] + # create global settings + GlobalSettings.get() + def get_connection(self): return connections[DEFAULT_DB_ALIAS] def test_all_max_queries(self): # set up some bookmarks with associated tags num_initial_bookmarks = 10 - for index in range(num_initial_bookmarks): + for _ in range(num_initial_bookmarks): self.setup_bookmark(tags=[self.setup_tag()]) # capture number of queries diff --git a/bookmarks/tests/test_nav_menu.py b/bookmarks/tests/test_layout.py similarity index 52% rename from bookmarks/tests/test_nav_menu.py rename to bookmarks/tests/test_layout.py index e6ce5d67..e7fb2636 100644 --- a/bookmarks/tests/test_nav_menu.py +++ b/bookmarks/tests/test_layout.py @@ -1,16 +1,17 @@ from django.test import TestCase from django.urls import reverse +from bookmarks.models import GlobalSettings from bookmarks.tests.helpers import BookmarkFactoryMixin -class NavMenuTestCase(TestCase, BookmarkFactoryMixin): +class LayoutTestCase(TestCase, BookmarkFactoryMixin): def setUp(self) -> None: user = self.get_or_create_test_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.save() response = self.client.get(reverse("bookmarks:index")) @@ -36,3 +37,29 @@ def test_should_respect_share_profile_setting(self): html, 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( + '', + html, + count=1, + ) + + settings.enable_link_prefetch = True + settings.save() + + response = self.client.get(reverse("bookmarks:index")) + html = response.content.decode() + + self.assertInHTML( + '', + html, + count=0, + ) diff --git a/bookmarks/tests/test_user_profile_middleware.py b/bookmarks/tests/test_linkding_middleware.py similarity index 96% rename from bookmarks/tests/test_user_profile_middleware.py rename to bookmarks/tests/test_linkding_middleware.py index 1713df00..9280b4ee 100644 --- a/bookmarks/tests/test_user_profile_middleware.py +++ b/bookmarks/tests/test_linkding_middleware.py @@ -6,7 +6,7 @@ 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): response = self.client.get(reverse("login")) diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py index 0f82726a..c9c2bd9a 100644 --- a/bookmarks/tests/test_settings_general_view.py +++ b/bookmarks/tests/test_settings_general_view.py @@ -79,6 +79,13 @@ def test_should_check_authentication(self): 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): form_data = { "update_profile": "", @@ -105,7 +112,9 @@ def test_update_profile(self): "custom_css": "body { background-color: #000; }", "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() self.user.profile.refresh_from_db() @@ -179,7 +188,9 @@ def test_update_profile_should_not_be_called_without_respective_form_action(self form_data = { "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() self.user.profile.refresh_from_db() @@ -199,14 +210,14 @@ def test_enable_favicons_should_schedule_icon_update(self): "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) # No update scheduled if favicons are already enabled 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() @@ -217,7 +228,7 @@ def test_enable_favicons_should_schedule_icon_update(self): } ) - 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() @@ -229,7 +240,7 @@ def test_refresh_favicons(self): "refresh_favicons": "", } response = self.client.post( - reverse("bookmarks:settings.general"), form_data + reverse("bookmarks:settings.update"), form_data, follow=True ) html = response.content.decode() @@ -243,9 +254,7 @@ def test_refresh_favicons_should_not_be_called_without_respective_form_action(se tasks, "schedule_refresh_favicons" ) as mock_schedule_refresh_favicons: form_data = {} - response = self.client.post( - reverse("bookmarks:settings.general"), form_data - ) + response = self.client.post(reverse("bookmarks:settings.update"), form_data) html = response.content.decode() mock_schedule_refresh_favicons.assert_not_called() @@ -315,14 +324,14 @@ def test_enable_preview_image_should_schedule_preview_update(self): "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) # No update scheduled if favicons are already enabled 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() @@ -333,7 +342,7 @@ def test_enable_preview_image_should_schedule_preview_update(self): } ) - 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() @@ -422,10 +431,11 @@ def test_create_missing_html_snapshots(self): "create_missing_html_snapshots": "", } response = self.client.post( - reverse("bookmarks:settings.general"), form_data + reverse("bookmarks:settings.update"), form_data, follow=True ) html = response.content.decode() + self.assertEqual(response.status_code, 200) mock_create_missing_html_snapshots.assert_called_once() self.assertSuccessMessage( html, "Queued 5 missing snapshots. This may take a while..." @@ -441,10 +451,11 @@ def test_create_missing_html_snapshots_no_missing_snapshots(self): "create_missing_html_snapshots": "", } response = self.client.post( - reverse("bookmarks:settings.general"), form_data + reverse("bookmarks:settings.update"), form_data, follow=True ) html = response.content.decode() + self.assertEqual(response.status_code, 200) mock_create_missing_html_snapshots.assert_called_once() self.assertSuccessMessage(html, "No missing snapshots found.") @@ -457,10 +468,11 @@ def test_create_missing_html_snapshots_should_not_be_called_without_respective_f mock_create_missing_html_snapshots.return_value = 5 form_data = {} response = self.client.post( - reverse("bookmarks:settings.general"), form_data + reverse("bookmarks:settings.update"), form_data, follow=True ) html = response.content.decode() + self.assertEqual(response.status_code, 200) mock_create_missing_html_snapshots.assert_not_called() self.assertSuccessMessage( html, "Queued 5 missing snapshots. This may take a while...", count=0 @@ -477,7 +489,9 @@ def test_update_global_settings(self): "landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS, "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.assertSuccessMessage(response.content.decode(), "Global settings updated") @@ -491,7 +505,9 @@ def test_update_global_settings(self): "landing_page": GlobalSettings.LANDING_PAGE_LOGIN, "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.assertSuccessMessage(response.content.decode(), "Global settings updated") @@ -509,7 +525,9 @@ def test_update_global_settings_should_not_be_called_without_respective_form_act form_data = { "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.assertSuccessMessage( response.content.decode(), "Global settings updated", count=0 @@ -520,7 +538,7 @@ def test_update_global_settings_checks_for_superuser(self): "update_global_settings": "", "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) def test_global_settings_only_visible_for_superuser(self): diff --git a/bookmarks/tests/test_settings_integrations_view.py b/bookmarks/tests/test_settings_integrations_view.py index 8098a960..5ada01e9 100644 --- a/bookmarks/tests/test_settings_integrations_view.py +++ b/bookmarks/tests/test_settings_integrations_view.py @@ -68,17 +68,18 @@ def test_should_display_feed_urls(self): token = FeedToken.objects.first() self.assertInHTML( - f'All bookmarks', html + f'All bookmarks', + html, ) self.assertInHTML( - f'Unread bookmarks', + f'Unread bookmarks', html, ) self.assertInHTML( - f'Shared bookmarks', + f'Shared bookmarks', html, ) self.assertInHTML( - f'Public shared bookmarks', + 'Public shared bookmarks', html, ) diff --git a/bookmarks/tests/test_tag_cloud_template.py b/bookmarks/tests/test_tag_cloud_template.py index 4ac807ea..d0e0f0d3 100644 --- a/bookmarks/tests/test_tag_cloud_template.py +++ b/bookmarks/tests/test_tag_cloud_template.py @@ -5,7 +5,7 @@ from django.template import Template, RequestContext from django.test import TestCase, RequestFactory -from bookmarks.middlewares import UserProfileMiddleware +from bookmarks.middlewares import LinkdingMiddleware from bookmarks.models import UserProfile from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin from bookmarks.views.partials import contexts @@ -21,7 +21,7 @@ def render_template( rf = RequestFactory() request = rf.get(url) request.user = user or self.get_or_create_test_user() - middleware = UserProfileMiddleware(lambda r: HttpResponse()) + middleware = LinkdingMiddleware(lambda r: HttpResponse()) middleware(request) tag_cloud_context = context_type(request) diff --git a/bookmarks/urls.py b/bookmarks/urls.py index fd855e38..74d082f0 100644 --- a/bookmarks/urls.py +++ b/bookmarks/urls.py @@ -106,6 +106,7 @@ # Settings path("settings", views.settings.general, name="settings.index"), path("settings/general", views.settings.general, name="settings.general"), + path("settings/update", views.settings.update, name="settings.update"), path( "settings/integrations", views.settings.integrations, diff --git a/bookmarks/views/bookmarks.py b/bookmarks/views/bookmarks.py index 2bcb653f..0e882813 100644 --- a/bookmarks/views/bookmarks.py +++ b/bookmarks/views/bookmarks.py @@ -189,6 +189,7 @@ def convert_tag_string(tag_string: str): @login_required def new(request): + status = 200 initial_url = request.GET.get("url") initial_title = request.GET.get("title") initial_description = request.GET.get("description") @@ -207,6 +208,8 @@ def new(request): return HttpResponseRedirect(reverse("bookmarks:close")) else: return HttpResponseRedirect(reverse("bookmarks:index")) + else: + status = 422 else: form = BookmarkForm() if initial_url: @@ -228,7 +231,7 @@ def new(request): "return_url": reverse("bookmarks:index"), } - return render(request, "bookmarks/new.html", context) + return render(request, "bookmarks/new.html", context, status=status) @login_required diff --git a/bookmarks/views/root.py b/bookmarks/views/root.py index 8b728466..71b421b4 100644 --- a/bookmarks/views/root.py +++ b/bookmarks/views/root.py @@ -7,7 +7,7 @@ def root(request): # Redirect unauthenticated users to the configured landing page if not request.user.is_authenticated: - settings = GlobalSettings.get() + settings = request.global_settings if settings.landing_page == GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS: return HttpResponseRedirect(reverse("bookmarks:shared")) diff --git a/bookmarks/views/settings.py b/bookmarks/views/settings.py index 3c8a1f58..3566d85f 100644 --- a/bookmarks/views/settings.py +++ b/bookmarks/views/settings.py @@ -29,41 +29,19 @@ @login_required def general(request): - profile_form = None - global_settings_form = None enable_refresh_favicons = django_settings.LD_ENABLE_REFRESH_FAVICONS has_snapshot_support = django_settings.LD_ENABLE_SNAPSHOTS 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( - messages.get_messages(request), "bookmark_import_errors" + messages.get_messages(request), "settings_error_message" ) version_info = get_version_info(get_ttl_hash()) - if request.method == "POST": - if "update_profile" in request.POST: - profile_form = update_profile(request) - 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: + profile_form = UserProfileForm(instance=request.user_profile) + global_settings_form = None + if request.user.is_superuser: global_settings_form = GlobalSettingsForm(instance=GlobalSettings.get()) 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): user = request.user profile = user.profile @@ -178,7 +190,7 @@ def bookmark_import(request): if import_file is None: 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")) @@ -186,21 +198,20 @@ def bookmark_import(request): content = import_file.read().decode() result = importer.import_netscape_html(content, request.user, import_options) 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: err_msg = ( str(result.failed) + " 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: logging.exception("Unexpected error during bookmark import") messages.error( request, "An error occurred during bookmark import.", - "bookmark_import_errors", + "settings_error_message", ) - pass return HttpResponseRedirect(reverse("bookmarks:settings.general")) diff --git a/package-lock.json b/package-lock.json index 0037f368..44a4a28d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "linkding", - "version": "1.31.1", + "version": "1.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "linkding", - "version": "1.31.1", + "version": "1.32.0", "license": "MIT", "dependencies": { + "@hotwired/turbo": "^8.0.6", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", "@rollup/wasm-node": "^4.13.0", @@ -79,6 +80,14 @@ "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": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", diff --git a/package.json b/package.json index 883f1bff..4cf1376f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "homepage": "https://github.com/sissbruecker/linkding#readme", "dependencies": { + "@hotwired/turbo": "^8.0.6", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", "@rollup/wasm-node": "^4.13.0", diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py index 12d04476..b701488f 100644 --- a/siteroot/settings/base.py +++ b/siteroot/settings/base.py @@ -52,7 +52,7 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", - "bookmarks.middlewares.UserProfileMiddleware", + "bookmarks.middlewares.LinkdingMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.locale.LocaleMiddleware",