mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-24 20:33:04 +00:00
Add configuration options for pagination (#835)
This commit is contained in:
parent
2aab2813f4
commit
450980a8d4
10 changed files with 157 additions and 10 deletions
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 5.0.8 on 2024-09-18 20:11
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookmarks", "0039_globalsettings_enable_link_prefetch"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userprofile",
|
||||||
|
name="items_per_page",
|
||||||
|
field=models.IntegerField(
|
||||||
|
default=30, validators=[django.core.validators.MinValueValidator(10)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userprofile",
|
||||||
|
name="sticky_pagination",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,12 +1,13 @@
|
||||||
|
import binascii
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import binascii
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
@ -422,6 +423,10 @@ class UserProfile(models.Model):
|
||||||
search_preferences = models.JSONField(default=dict, null=False)
|
search_preferences = models.JSONField(default=dict, null=False)
|
||||||
enable_automatic_html_snapshots = models.BooleanField(default=True, null=False)
|
enable_automatic_html_snapshots = models.BooleanField(default=True, null=False)
|
||||||
default_mark_unread = models.BooleanField(default=False, null=False)
|
default_mark_unread = models.BooleanField(default=False, null=False)
|
||||||
|
items_per_page = models.IntegerField(
|
||||||
|
null=False, default=30, validators=[MinValueValidator(10)]
|
||||||
|
)
|
||||||
|
sticky_pagination = models.BooleanField(default=False, null=False)
|
||||||
|
|
||||||
|
|
||||||
class UserProfileForm(forms.ModelForm):
|
class UserProfileForm(forms.ModelForm):
|
||||||
|
@ -450,6 +455,8 @@ class UserProfileForm(forms.ModelForm):
|
||||||
"default_mark_unread",
|
"default_mark_unread",
|
||||||
"custom_css",
|
"custom_css",
|
||||||
"auto_tagging_rules",
|
"auto_tagging_rules",
|
||||||
|
"items_per_page",
|
||||||
|
"sticky_pagination",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -300,6 +300,28 @@ li[ld-bookmark-item] {
|
||||||
& .page-item:first-child a {
|
& .page-item:first-child a {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.sticky {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
border-top: solid 1px var(--secondary-border-color);
|
||||||
|
background: var(--body-color);
|
||||||
|
padding-bottom: var(--unit-h);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: calc(-1 * calc(var(--bulk-edit-toggle-width) + var(--bulk-edit-toggle-offset)));
|
||||||
|
width: calc(var(--bulk-edit-toggle-width) + var(--bulk-edit-toggle-offset));
|
||||||
|
background: var(--body-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .pagination {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-cloud {
|
.tag-cloud {
|
||||||
|
@ -379,6 +401,7 @@ ul.bookmark-list {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide section border when bulk edit bar is opened, otherwise borders overlap in dark mode due to using contrast colors */
|
/* Hide section border when bulk edit bar is opened, otherwise borders overlap in dark mode due to using contrast colors */
|
||||||
|
|
||||||
&.active section:first-of-type .content-area-header {
|
&.active section:first-of-type .content-area-header {
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
}
|
}
|
||||||
|
@ -389,6 +412,19 @@ ul.bookmark-list {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* make sticky pagination expand to cover checkboxes to the left */
|
||||||
|
|
||||||
|
&.active .bookmark-pagination.sticky:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
bottom: 0;
|
||||||
|
left: calc(-1 * calc(var(--bulk-edit-toggle-width) + var(--bulk-edit-toggle-offset)));
|
||||||
|
width: calc(var(--bulk-edit-toggle-width) + var(--bulk-edit-toggle-offset));
|
||||||
|
background: var(--body-color);
|
||||||
|
border-top: solid 1px var(--secondary-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* All checkbox */
|
/* All checkbox */
|
||||||
|
|
||||||
& .form-checkbox.bulk-edit-checkbox.all {
|
& .form-checkbox.bulk-edit-checkbox.all {
|
||||||
|
|
|
@ -149,7 +149,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="bookmark-pagination">
|
<div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}">
|
||||||
{% pagination bookmark_list.bookmarks_page %}
|
{% pagination bookmark_list.bookmarks_page %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -101,6 +101,29 @@
|
||||||
Whether to open bookmarks a new page or in the same page.
|
Whether to open bookmarks a new page or in the same page.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group{% if form.items_per_page.errors %} has-error{% endif %}">
|
||||||
|
<label for="{{ form.items_per_page.id_for_label }}" class="form-label">Items per page</label>
|
||||||
|
{{ form.items_per_page|add_class:"form-input width-25 width-sm-100"|attr:"min:10" }}
|
||||||
|
{% if form.items_per_page.errors %}
|
||||||
|
<div class="form-input-hint is-error">
|
||||||
|
{{ form.items_per_page.errors }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-input-hint">
|
||||||
|
The number of bookmarks to display per page.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.sticky_pagination.id_for_label }}" class="form-checkbox">
|
||||||
|
{{ form.sticky_pagination }}
|
||||||
|
<i class="form-icon"></i> Sticky pagination
|
||||||
|
</label>
|
||||||
|
<div class="form-input-hint">
|
||||||
|
When enabled, the pagination controls will stick to the bottom of the screen, so that they are always
|
||||||
|
visible without having to scroll to the end of the page first.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.tag_search.id_for_label }}" class="form-label">Tag search</label>
|
<label for="{{ form.tag_search.id_for_label }}" class="form-label">Tag search</label>
|
||||||
{{ form.tag_search|add_class:"form-select width-25 width-sm-100" }}
|
{{ form.tag_search|add_class:"form-select width-25 width-sm-100" }}
|
||||||
|
|
|
@ -955,3 +955,37 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
'<p class="empty-title h5">You have no bookmarks yet</p>', html
|
'<p class="empty-title h5">You have no bookmarks yet</p>', html
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_pagination_is_not_sticky_by_default(self):
|
||||||
|
self.setup_bookmark()
|
||||||
|
html = self.render_template()
|
||||||
|
|
||||||
|
self.assertIn('<div class="bookmark-pagination">', html)
|
||||||
|
|
||||||
|
def test_pagination_is_sticky_when_enabled_in_profile(self):
|
||||||
|
self.setup_bookmark()
|
||||||
|
profile = self.get_or_create_test_user().profile
|
||||||
|
profile.sticky_pagination = True
|
||||||
|
profile.save()
|
||||||
|
html = self.render_template()
|
||||||
|
|
||||||
|
self.assertIn('<div class="bookmark-pagination sticky">', html)
|
||||||
|
|
||||||
|
def test_items_per_page_is_30_by_default(self):
|
||||||
|
self.setup_numbered_bookmarks(50)
|
||||||
|
html = self.render_template()
|
||||||
|
|
||||||
|
soup = self.make_soup(html)
|
||||||
|
bookmarks = soup.select("li[ld-bookmark-item]")
|
||||||
|
self.assertEqual(30, len(bookmarks))
|
||||||
|
|
||||||
|
def test_items_per_page_is_configurable(self):
|
||||||
|
self.setup_numbered_bookmarks(50)
|
||||||
|
profile = self.get_or_create_test_user().profile
|
||||||
|
profile.items_per_page = 10
|
||||||
|
profile.save()
|
||||||
|
html = self.render_template()
|
||||||
|
|
||||||
|
soup = self.make_soup(html)
|
||||||
|
bookmarks = soup.select("li[ld-bookmark-item]")
|
||||||
|
self.assertEqual(10, len(bookmarks))
|
||||||
|
|
|
@ -43,6 +43,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
"permanent_notes": False,
|
"permanent_notes": False,
|
||||||
"custom_css": "",
|
"custom_css": "",
|
||||||
"auto_tagging_rules": "",
|
"auto_tagging_rules": "",
|
||||||
|
"items_per_page": "30",
|
||||||
|
"sticky_pagination": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {**form_data, **overrides}
|
return {**form_data, **overrides}
|
||||||
|
@ -111,6 +113,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
"default_mark_unread": True,
|
"default_mark_unread": True,
|
||||||
"custom_css": "body { background-color: #000; }",
|
"custom_css": "body { background-color: #000; }",
|
||||||
"auto_tagging_rules": "example.com tag",
|
"auto_tagging_rules": "example.com tag",
|
||||||
|
"items_per_page": "10",
|
||||||
|
"sticky_pagination": True,
|
||||||
}
|
}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("bookmarks:settings.update"), form_data, follow=True
|
reverse("bookmarks:settings.update"), form_data, follow=True
|
||||||
|
@ -182,6 +186,13 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.user.profile.auto_tagging_rules, form_data["auto_tagging_rules"]
|
self.user.profile.auto_tagging_rules, form_data["auto_tagging_rules"]
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.user.profile.items_per_page, int(form_data["items_per_page"])
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.user.profile.sticky_pagination, form_data["sticky_pagination"]
|
||||||
|
)
|
||||||
|
|
||||||
self.assertSuccessMessage(html, "Profile updated")
|
self.assertSuccessMessage(html, "Profile updated")
|
||||||
|
|
||||||
def test_update_profile_should_not_be_called_without_respective_form_action(self):
|
def test_update_profile_should_not_be_called_without_respective_form_action(self):
|
||||||
|
|
|
@ -38,8 +38,6 @@ from bookmarks.services.bookmarks import (
|
||||||
from bookmarks.utils import get_safe_return_url
|
from bookmarks.utils import get_safe_return_url
|
||||||
from bookmarks.views import contexts, partials, turbo
|
from bookmarks.views import contexts, partials, turbo
|
||||||
|
|
||||||
_default_page_size = 30
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
|
|
|
@ -21,7 +21,6 @@ from bookmarks.models import (
|
||||||
)
|
)
|
||||||
from bookmarks.services.wayback import generate_fallback_webarchive_url
|
from bookmarks.services.wayback import generate_fallback_webarchive_url
|
||||||
|
|
||||||
DEFAULT_PAGE_SIZE = 30
|
|
||||||
CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
|
CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
|
||||||
|
|
||||||
|
|
||||||
|
@ -181,7 +180,7 @@ class BookmarkListContext:
|
||||||
|
|
||||||
query_set = request_context.get_bookmark_query_set(self.search)
|
query_set = request_context.get_bookmark_query_set(self.search)
|
||||||
page_number = request.GET.get("page")
|
page_number = request.GET.get("page")
|
||||||
paginator = Paginator(query_set, DEFAULT_PAGE_SIZE)
|
paginator = Paginator(query_set, user_profile.items_per_page)
|
||||||
bookmarks_page = paginator.get_page(page_number)
|
bookmarks_page = paginator.get_page(page_number)
|
||||||
# Prefetch related objects, this avoids n+1 queries when accessing fields in templates
|
# Prefetch related objects, this avoids n+1 queries when accessing fields in templates
|
||||||
models.prefetch_related_objects(bookmarks_page.object_list, "owner", "tags")
|
models.prefetch_related_objects(bookmarks_page.object_list, "owner", "tags")
|
||||||
|
|
|
@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def general(request):
|
def general(request, status=200, context_overrides=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(
|
||||||
|
@ -44,6 +44,9 @@ def general(request):
|
||||||
if request.user.is_superuser:
|
if request.user.is_superuser:
|
||||||
global_settings_form = GlobalSettingsForm(instance=GlobalSettings.get())
|
global_settings_form = GlobalSettingsForm(instance=GlobalSettings.get())
|
||||||
|
|
||||||
|
if context_overrides is None:
|
||||||
|
context_overrides = {}
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"settings/general.html",
|
"settings/general.html",
|
||||||
|
@ -55,7 +58,9 @@ def general(request):
|
||||||
"success_message": success_message,
|
"success_message": success_message,
|
||||||
"error_message": error_message,
|
"error_message": error_message,
|
||||||
"version_info": version_info,
|
"version_info": version_info,
|
||||||
|
**context_overrides,
|
||||||
},
|
},
|
||||||
|
status=status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,8 +68,7 @@ def general(request):
|
||||||
def update(request):
|
def update(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if "update_profile" in request.POST:
|
if "update_profile" in request.POST:
|
||||||
update_profile(request)
|
return update_profile(request)
|
||||||
messages.success(request, "Profile updated", "settings_success_message")
|
|
||||||
if "update_global_settings" in request.POST:
|
if "update_global_settings" in request.POST:
|
||||||
update_global_settings(request)
|
update_global_settings(request)
|
||||||
messages.success(
|
messages.success(
|
||||||
|
@ -101,13 +105,22 @@ def update_profile(request):
|
||||||
form = UserProfileForm(request.POST, instance=profile)
|
form = UserProfileForm(request.POST, instance=profile)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
|
messages.success(request, "Profile updated", "settings_success_message")
|
||||||
# Load missing favicons if the feature was just enabled
|
# Load missing favicons if the feature was just enabled
|
||||||
if profile.enable_favicons and not favicons_were_enabled:
|
if profile.enable_favicons and not favicons_were_enabled:
|
||||||
tasks.schedule_bookmarks_without_favicons(request.user)
|
tasks.schedule_bookmarks_without_favicons(request.user)
|
||||||
# Load missing preview images if the feature was just enabled
|
# Load missing preview images if the feature was just enabled
|
||||||
if profile.enable_preview_images and not previews_were_enabled:
|
if profile.enable_preview_images and not previews_were_enabled:
|
||||||
tasks.schedule_bookmarks_without_previews(request.user)
|
tasks.schedule_bookmarks_without_previews(request.user)
|
||||||
return form
|
|
||||||
|
return HttpResponseRedirect(reverse("bookmarks:settings.general"))
|
||||||
|
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Profile update failed, check the form below for errors",
|
||||||
|
"settings_error_message",
|
||||||
|
)
|
||||||
|
return general(request, 422, {"form": form})
|
||||||
|
|
||||||
|
|
||||||
def update_global_settings(request):
|
def update_global_settings(request):
|
||||||
|
|
Loading…
Reference in a new issue