mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-10 06:04:15 +00:00
Add pagination (#63)
* Add pagination tag (#11) * Add pagination tag tests (#11) * Improve styling (#11)
This commit is contained in:
parent
b2aeec2cac
commit
f8fc360d84
6 changed files with 220 additions and 9 deletions
|
@ -48,3 +48,8 @@ h2 {
|
|||
.container > .columns > .column:not(:first-child) {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
// Remove left padding from first pagination link
|
||||
.pagination .page-item:first-child a {
|
||||
padding-left: 0;
|
||||
}
|
|
@ -38,6 +38,10 @@ ul.bookmark-list {
|
|||
}
|
||||
}
|
||||
|
||||
.bookmark-pagination {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.tag-cloud {
|
||||
|
||||
a {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load shared %}
|
||||
{% load pagination %}
|
||||
|
||||
<ul class="bookmark-list">
|
||||
{% for bookmark in bookmarks %}
|
||||
|
@ -30,13 +31,7 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="pagination">
|
||||
{% if bookmarks.has_next %}
|
||||
<a href="?{% update_query_string page=bookmarks.next_page_number %}"
|
||||
class="btn mr-2">< Older</a>
|
||||
{% endif %}
|
||||
{% if bookmarks.has_previous %}
|
||||
<a href="?{% update_query_string page=bookmarks.previous_page_number %}"
|
||||
class="btn">Newer ></a>
|
||||
{% endif %}
|
||||
|
||||
<div class="bookmark-pagination">
|
||||
{% pagination bookmarks %}
|
||||
</div>
|
||||
|
|
35
bookmarks/templates/bookmarks/pagination.html
Normal file
35
bookmarks/templates/bookmarks/pagination.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% load shared %}
|
||||
|
||||
<ul class="pagination">
|
||||
{% if page.has_previous %}
|
||||
<li class="page-item">
|
||||
<a href="?{% update_query_string page=page.previous_page_number %}" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for page_number in visible_page_numbers %}
|
||||
{% if page_number >= 0 %}
|
||||
<li class="page-item {% if page.number == page_number %}active{% endif %}">
|
||||
<a href="?{% update_query_string page=page_number %}">{{ page_number }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<span>...</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page.has_next %}
|
||||
<li class="page-item">
|
||||
<a href="?{% update_query_string page=page.next_page_number %}" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
55
bookmarks/templatetags/pagination.py
Normal file
55
bookmarks/templatetags/pagination.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
from functools import reduce
|
||||
|
||||
from django import template
|
||||
from django.core.paginator import Page
|
||||
|
||||
NUM_ADJACENT_PAGES = 2
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag('bookmarks/pagination.html', name='pagination', takes_context=True)
|
||||
def pagination(context, page: Page):
|
||||
visible_page_numbers = get_visible_page_numbers(page.number, page.paginator.num_pages)
|
||||
|
||||
return {
|
||||
'page': page,
|
||||
'visible_page_numbers': visible_page_numbers
|
||||
}
|
||||
|
||||
|
||||
def get_visible_page_numbers(current_page_number: int, num_pages: int) -> [int]:
|
||||
"""
|
||||
Generates a list of page indexes that should be rendered
|
||||
The list can contain "holes" which indicate that a range of pages are truncated
|
||||
Holes are indicated with a value of `-1`
|
||||
:param current_page_number:
|
||||
:param num_pages:
|
||||
"""
|
||||
visible_pages = set()
|
||||
|
||||
# Add adjacent pages around current page
|
||||
visible_pages |= set(range(
|
||||
max(1, current_page_number - NUM_ADJACENT_PAGES),
|
||||
min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1
|
||||
))
|
||||
|
||||
# Add first page
|
||||
visible_pages.add(1)
|
||||
|
||||
# Add last page
|
||||
visible_pages.add(num_pages)
|
||||
|
||||
# Convert to sorted list
|
||||
visible_pages = list(visible_pages)
|
||||
visible_pages.sort()
|
||||
|
||||
def append_page(result: [int], page_number: int):
|
||||
# Look for holes and insert a -1 as indicator
|
||||
is_hole = len(result) > 0 and result[-1] < page_number - 1
|
||||
if is_hole:
|
||||
result.append(-1)
|
||||
result.append(page_number)
|
||||
return result
|
||||
|
||||
return reduce(append_page, visible_pages, [])
|
117
bookmarks/tests/test_pagination_tag.py
Normal file
117
bookmarks/tests/test_pagination_tag.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
from django.core.paginator import Paginator
|
||||
from django.test import SimpleTestCase, RequestFactory
|
||||
from django.template import Template, RequestContext
|
||||
|
||||
|
||||
class PaginationTagTest(SimpleTestCase):
|
||||
|
||||
def render_template(self, num_items: int, page_size: int, current_page: int, url: str = '/test') -> str:
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
paginator = Paginator(range(0, num_items), page_size)
|
||||
page = paginator.page(current_page)
|
||||
|
||||
context = RequestContext(request, {'page': page})
|
||||
template_to_render = Template(
|
||||
'{% load pagination %}'
|
||||
'{% pagination page %}'
|
||||
)
|
||||
return template_to_render.render(context)
|
||||
|
||||
def assertPrevLinkDisabled(self, html: str):
|
||||
self.assertInHTML('''
|
||||
<li class="page-item disabled">
|
||||
<a href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
''', html)
|
||||
|
||||
def assertPrevLink(self, html: str, page_number: int, href: str = None):
|
||||
href = href if href else '?page={0}'.format(page_number)
|
||||
self.assertInHTML('''
|
||||
<li class="page-item">
|
||||
<a href="{0}" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
'''.format(href), html)
|
||||
|
||||
def assertNextLinkDisabled(self, html: str):
|
||||
self.assertInHTML('''
|
||||
<li class="page-item disabled">
|
||||
<a href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
''', html)
|
||||
|
||||
def assertNextLink(self, html: str, page_number: int, href: str = None):
|
||||
href = href if href else '?page={0}'.format(page_number)
|
||||
self.assertInHTML('''
|
||||
<li class="page-item">
|
||||
<a href="{0}" tabindex="-1">Next</a>
|
||||
</li>
|
||||
'''.format(href), html)
|
||||
|
||||
def assertPageLink(self, html: str, page_number: int, active: bool, count: int = 1, href: str = None):
|
||||
active_class = 'active' if active else ''
|
||||
href = href if href else '?page={0}'.format(page_number)
|
||||
self.assertInHTML('''
|
||||
<li class="page-item {1}">
|
||||
<a href="{2}">{0}</a>
|
||||
</li>
|
||||
'''.format(page_number, active_class, href), html, count=count)
|
||||
|
||||
def assertTruncationIndicators(self, html: str, count: int):
|
||||
self.assertInHTML('''
|
||||
<li class="page-item">
|
||||
<span>...</span>
|
||||
</li>
|
||||
''', html, count=count)
|
||||
|
||||
def test_previous_disabled_on_page_1(self):
|
||||
rendered_template = self.render_template(100, 10, 1)
|
||||
self.assertPrevLinkDisabled(rendered_template)
|
||||
|
||||
def test_previous_enabled_after_page_1(self):
|
||||
for page_number in range(2, 10):
|
||||
rendered_template = self.render_template(100, 10, page_number)
|
||||
self.assertPrevLink(rendered_template, page_number - 1)
|
||||
|
||||
def test_next_disabled_on_last_page(self):
|
||||
rendered_template = self.render_template(100, 10, 10)
|
||||
self.assertNextLinkDisabled(rendered_template)
|
||||
|
||||
def test_next_enabled_before_last_page(self):
|
||||
for page_number in range(1, 9):
|
||||
rendered_template = self.render_template(100, 10, page_number)
|
||||
self.assertNextLink(rendered_template, page_number + 1)
|
||||
|
||||
def test_truncate_pages_start(self):
|
||||
current_page = 1
|
||||
expected_visible_pages = [1, 2, 3, 10]
|
||||
rendered_template = self.render_template(100, 10, current_page)
|
||||
for page_number in range(1, 10):
|
||||
expected_occurrences = 1 if page_number in expected_visible_pages else 0
|
||||
self.assertPageLink(rendered_template, page_number, page_number == current_page, expected_occurrences)
|
||||
self.assertTruncationIndicators(rendered_template, 1)
|
||||
|
||||
def test_truncate_pages_middle(self):
|
||||
current_page = 5
|
||||
expected_visible_pages = [1, 3, 4, 5, 6, 7, 10]
|
||||
rendered_template = self.render_template(100, 10, current_page)
|
||||
for page_number in range(1, 10):
|
||||
expected_occurrences = 1 if page_number in expected_visible_pages else 0
|
||||
self.assertPageLink(rendered_template, page_number, page_number == current_page, expected_occurrences)
|
||||
self.assertTruncationIndicators(rendered_template, 2)
|
||||
|
||||
def test_truncate_pages_near_end(self):
|
||||
current_page = 9
|
||||
expected_visible_pages = [1, 7, 8, 9, 10]
|
||||
rendered_template = self.render_template(100, 10, current_page)
|
||||
for page_number in range(1, 10):
|
||||
expected_occurrences = 1 if page_number in expected_visible_pages else 0
|
||||
self.assertPageLink(rendered_template, page_number, page_number == current_page, expected_occurrences)
|
||||
self.assertTruncationIndicators(rendered_template, 1)
|
||||
|
||||
def test_extend_existing_query(self):
|
||||
rendered_template = self.render_template(100, 10, 2, url='/test?q=cake')
|
||||
self.assertPrevLink(rendered_template, 1, href='?q=cake&page=1')
|
||||
self.assertPageLink(rendered_template, 1, False, href='?q=cake&page=1')
|
||||
self.assertPageLink(rendered_template, 2, True, href='?q=cake&page=2')
|
||||
self.assertNextLink(rendered_template, 3, href='?q=cake&page=3')
|
Loading…
Reference in a new issue