Compare commits

...

4 Commits

Author SHA1 Message Date
Sascha Ißbrücker
63acde36de Bump version 2021-01-12 22:43:54 +01:00
Sascha Ißbrücker
70953a52b9 Fix duplicate tag error (#65) 2021-01-12 22:42:56 +01:00
Sascha Ißbrücker
f8fc360d84 Add pagination (#63)
* Add pagination tag (#11)

* Add pagination tag tests (#11)

* Improve styling (#11)
2021-01-11 17:49:53 +01:00
Sascha Ißbrücker
b2aeec2cac Update CHANGELOG.md 2021-01-09 22:19:37 +01:00
12 changed files with 260 additions and 19 deletions

View File

@@ -1,8 +1,13 @@
# Changelog
## v1.2.0 (09/01/2021)
- [**closed**] Add Favicon [#58](https://github.com/sissbruecker/linkding/issues/58)
- [**closed**] Make tags case-insensitive [#45](https://github.com/sissbruecker/linkding/issues/45)
---
## v1.1.1 (01/01/2021)
- [**enhancement**] Add docker-compose support [#54](https://github.com/sissbruecker/linkding/pull/54)
---
## v1.1.0 (31/12/2020)

View File

@@ -1,3 +1,4 @@
import logging
import operator
from typing import List
@@ -7,6 +8,8 @@ from django.utils import timezone
from bookmarks.models import Tag
from bookmarks.utils import unique
logger = logging.getLogger(__name__)
def get_or_create_tags(tag_names: List[str], user: User):
tags = [get_or_create_tag(tag_name, user) for tag_name in tag_names]
@@ -21,3 +24,14 @@ def get_or_create_tag(name: str, user: User):
tag.date_added = timezone.now()
tag.save()
return tag
except Tag.MultipleObjectsReturned:
# Legacy databases might contain duplicate tags with different capitalization
first_tag = Tag.objects.filter(name__iexact=name, owner=user).first()
message = (
"Found multiple tags for the name '{0}' with different capitalization. "
"Using the first tag with the name '{1}'. "
"Since v.1.2 tags work case-insensitive, which means duplicates of the same name are not allowed anymore. "
"To solve this error remove the duplicate tag in admin."
).format(name, first_tag.name)
logger.error(message)
return first_tag

View File

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

View File

@@ -38,6 +38,10 @@ ul.bookmark-list {
}
}
.bookmark-pagination {
margin-top: 1rem;
}
.tag-cloud {
a {

View File

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

View 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>

View 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, [])

View 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')

View File

@@ -42,6 +42,13 @@ class TagTestCase(TestCase):
self.assertEqual(len(tags), 1)
self.assertEqual(first_tag.id, second_tag.id)
def test_get_or_create_tag_should_handle_legacy_dbs_with_existing_duplicates(self):
Tag.objects.create(name='book', date_added=timezone.now(), owner=self.user)
Tag.objects.create(name='Book', date_added=timezone.now(), owner=self.user)
first_tag = get_or_create_tag('Book', self.user)
self.assertEqual(first_tag.id, first_tag.id)
def test_get_or_create_tags_should_return_tags(self):
books_tag = get_or_create_tag('Book', self.user)
movies_tag = get_or_create_tag('Movie', self.user)

View File

@@ -1,6 +1,6 @@
{
"name": "linkding",
"version": "1.2.0",
"version": "1.2.1",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -12,21 +12,25 @@ DEBUG = True
SASS_PROCESSOR_ENABLED = True
# Enable debug logging
# Logging with SQL statements
LOGGING = {
'version': 1,
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
}
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'root': {
'handlers': ['console'],
'level': 'WARNING',
},
'loggers': {
'django.db.backends': {
'level': 'ERROR', # Set to DEBUG to log all SQL calls

View File

@@ -1 +1 @@
1.2.0
1.2.1