Add configuration options for pagination (#835)

This commit is contained in:
Sascha Ißbrücker 2024-09-18 23:14:19 +02:00 committed by GitHub
parent 2aab2813f4
commit 450980a8d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 157 additions and 10 deletions

View file

@ -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),
),
]

View file

@ -1,12 +1,13 @@
import binascii
import logging
import os
from typing import List
import binascii
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
@ -422,6 +423,10 @@ class UserProfile(models.Model):
search_preferences = models.JSONField(default=dict, null=False)
enable_automatic_html_snapshots = models.BooleanField(default=True, 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):
@ -450,6 +455,8 @@ class UserProfileForm(forms.ModelForm):
"default_mark_unread",
"custom_css",
"auto_tagging_rules",
"items_per_page",
"sticky_pagination",
]

View file

@ -300,6 +300,28 @@ li[ld-bookmark-item] {
& .page-item:first-child a {
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 {
@ -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 */
&.active section:first-of-type .content-area-header {
border-bottom-color: transparent;
}
@ -389,6 +412,19 @@ ul.bookmark-list {
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 */
& .form-checkbox.bulk-edit-checkbox.all {

View file

@ -149,7 +149,7 @@
{% endfor %}
</ul>
<div class="bookmark-pagination">
<div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}">
{% pagination bookmark_list.bookmarks_page %}
</div>
{% endif %}

View file

@ -101,6 +101,29 @@
Whether to open bookmarks a new page or in the same page.
</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">
<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" }}

View file

@ -955,3 +955,37 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertInHTML(
'<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))

View file

@ -43,6 +43,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"permanent_notes": False,
"custom_css": "",
"auto_tagging_rules": "",
"items_per_page": "30",
"sticky_pagination": False,
}
return {**form_data, **overrides}
@ -111,6 +113,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"default_mark_unread": True,
"custom_css": "body { background-color: #000; }",
"auto_tagging_rules": "example.com tag",
"items_per_page": "10",
"sticky_pagination": True,
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
@ -182,6 +186,13 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(
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")
def test_update_profile_should_not_be_called_without_respective_form_action(self):

View file

@ -38,8 +38,6 @@ from bookmarks.services.bookmarks import (
from bookmarks.utils import get_safe_return_url
from bookmarks.views import contexts, partials, turbo
_default_page_size = 30
@login_required
def index(request):

View file

@ -21,7 +21,6 @@ from bookmarks.models import (
)
from bookmarks.services.wayback import generate_fallback_webarchive_url
DEFAULT_PAGE_SIZE = 30
CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
@ -181,7 +180,7 @@ class BookmarkListContext:
query_set = request_context.get_bookmark_query_set(self.search)
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)
# Prefetch related objects, this avoids n+1 queries when accessing fields in templates
models.prefetch_related_objects(bookmarks_page.object_list, "owner", "tags")

View file

@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
@login_required
def general(request):
def general(request, status=200, context_overrides=None):
enable_refresh_favicons = django_settings.LD_ENABLE_REFRESH_FAVICONS
has_snapshot_support = django_settings.LD_ENABLE_SNAPSHOTS
success_message = _find_message_with_tag(
@ -44,6 +44,9 @@ def general(request):
if request.user.is_superuser:
global_settings_form = GlobalSettingsForm(instance=GlobalSettings.get())
if context_overrides is None:
context_overrides = {}
return render(
request,
"settings/general.html",
@ -55,7 +58,9 @@ def general(request):
"success_message": success_message,
"error_message": error_message,
"version_info": version_info,
**context_overrides,
},
status=status,
)
@ -63,8 +68,7 @@ def general(request):
def update(request):
if request.method == "POST":
if "update_profile" in request.POST:
update_profile(request)
messages.success(request, "Profile updated", "settings_success_message")
return update_profile(request)
if "update_global_settings" in request.POST:
update_global_settings(request)
messages.success(
@ -101,13 +105,22 @@ def update_profile(request):
form = UserProfileForm(request.POST, instance=profile)
if form.is_valid():
form.save()
messages.success(request, "Profile updated", "settings_success_message")
# Load missing favicons if the feature was just enabled
if profile.enable_favicons and not favicons_were_enabled:
tasks.schedule_bookmarks_without_favicons(request.user)
# Load missing preview images if the feature was just enabled
if profile.enable_preview_images and not previews_were_enabled:
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):