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 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",
] ]

View file

@ -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 {

View file

@ -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 %}

View file

@ -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" }}

View file

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

View file

@ -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):

View file

@ -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):

View file

@ -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")

View file

@ -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):