mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-24 20:33:04 +00:00
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:
parent
3ae9cf0420
commit
c929e8f11c
29 changed files with 283 additions and 144 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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 #}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
|
@ -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"))
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
13
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue