mirror of
https://github.com/sissbruecker/linkding
synced 2025-02-16 12:28:23 +00:00
Display date_added in bookmark list (#85)
* Display date_added in bookmark list (#85) * Allow switching between different types of date formats * Improve date formatting * Use pluralize * Fix comment * Fix styles Co-authored-by: Sascha Ißbrücker <sissbruecker@lyska.io>
This commit is contained in:
parent
8dd1575dc6
commit
7a68a4abed
15 changed files with 226 additions and 9 deletions
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.18 on 2021-03-30 10:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bookmarks', '0007_userprofile'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='bookmark_date_display',
|
||||||
|
field=models.CharField(choices=[('relative', 'Relative'), ('absolute', 'Absolute'), ('hidden', 'Hidden')], default='relative', max_length=10),
|
||||||
|
),
|
||||||
|
]
|
|
@ -107,14 +107,24 @@ class UserProfile(models.Model):
|
||||||
(THEME_LIGHT, 'Light'),
|
(THEME_LIGHT, 'Light'),
|
||||||
(THEME_DARK, 'Dark'),
|
(THEME_DARK, 'Dark'),
|
||||||
]
|
]
|
||||||
|
BOOKMARK_DATE_DISPLAY_RELATIVE = 'relative'
|
||||||
|
BOOKMARK_DATE_DISPLAY_ABSOLUTE = 'absolute'
|
||||||
|
BOOKMARK_DATE_DISPLAY_HIDDEN = 'hidden'
|
||||||
|
BOOKMARK_DATE_DISPLAY_CHOICES = [
|
||||||
|
(BOOKMARK_DATE_DISPLAY_RELATIVE, 'Relative'),
|
||||||
|
(BOOKMARK_DATE_DISPLAY_ABSOLUTE, 'Absolute'),
|
||||||
|
(BOOKMARK_DATE_DISPLAY_HIDDEN, 'Hidden'),
|
||||||
|
]
|
||||||
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE)
|
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE)
|
||||||
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO)
|
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO)
|
||||||
|
bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, blank=False,
|
||||||
|
default=BOOKMARK_DATE_DISPLAY_RELATIVE)
|
||||||
|
|
||||||
|
|
||||||
class UserProfileForm(forms.ModelForm):
|
class UserProfileForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserProfile
|
model = UserProfile
|
||||||
fields = ['theme']
|
fields = ['theme', 'bookmark_date_display']
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=get_user_model())
|
@receiver(post_save, sender=get_user_model())
|
||||||
|
|
|
@ -55,8 +55,8 @@ def _base_bookmarks_query(user: User, query_string: str) -> QuerySet:
|
||||||
tags__name__iexact=tag_name
|
tags__name__iexact=tag_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sort by modification date
|
# Sort by date added
|
||||||
query_set = query_set.order_by('-date_modified')
|
query_set = query_set.order_by('-date_added')
|
||||||
|
|
||||||
return query_set
|
return query_set
|
||||||
|
|
||||||
|
|
|
@ -50,16 +50,22 @@ ul.bookmark-list {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions > *:not(:last-child) {
|
||||||
|
margin-right: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.actions .btn-link {
|
.actions .btn-link {
|
||||||
color: $gray-color;
|
color: $gray-color;
|
||||||
padding-left: 0;
|
padding: 0;
|
||||||
padding-right: 0;
|
height: auto;
|
||||||
|
vertical-align: unset;
|
||||||
|
border: none;
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&.active {
|
&.active {
|
||||||
color: darken($gray-color, 10%);
|
color: $gray-color-dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,4 +208,4 @@ $bulk-edit-transition-duration: 400ms;
|
||||||
#bulk-edit-mode:checked ~ .bookmarks-page .bulk-edit-bar {
|
#bulk-edit-mode:checked ~ .bookmarks-page .bulk-edit-bar {
|
||||||
max-height: 37px;
|
max-height: 37px;
|
||||||
border-bottom: solid 1px $border-color;
|
border-bottom: solid 1px $border-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,4 +14,4 @@
|
||||||
|
|
||||||
.text-gray-dark {
|
.text-gray-dark {
|
||||||
color: $gray-color-dark;
|
color: $gray-color-dark;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,14 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
{% if request.user.profile.bookmark_date_display == 'relative' %}
|
||||||
|
<span class="text-gray text-sm">{{ bookmark.date_added|humanize_relative_date }}</span>
|
||||||
|
<span class="text-gray text-sm">|</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.user.profile.bookmark_date_display == 'absolute' %}
|
||||||
|
<span class="text-gray text-sm">{{ bookmark.date_added|humanize_absolute_date }}</span>
|
||||||
|
<span class="text-gray text-sm">|</span>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}"
|
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}"
|
||||||
class="btn btn-link btn-sm">Edit</a>
|
class="btn btn-link btn-sm">Edit</a>
|
||||||
{% if bookmark.is_archived %}
|
{% if bookmark.is_archived %}
|
||||||
|
@ -44,4 +52,4 @@
|
||||||
|
|
||||||
<div class="bookmark-pagination">
|
<div class="bookmark-pagination">
|
||||||
{% pagination bookmarks %}
|
{% pagination bookmarks %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,6 +15,10 @@
|
||||||
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
|
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
|
||||||
{{ form.theme|add_class:"form-select col-2 col-sm-12" }}
|
{{ form.theme|add_class:"form-select col-2 col-sm-12" }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.bookmark_date_display.id_for_label }}" class="form-label">Bookmark date format</label>
|
||||||
|
{{ form.bookmark_date_display|add_class:"form-select col-2 col-sm-12" }}
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="submit" value="Save" class="btn btn-primary mt-2">
|
<input type="submit" value="Save" class="btn btn-primary mt-2">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -53,6 +53,7 @@ def tag_cloud(context, tags: List[Tag]):
|
||||||
@register.inclusion_tag('bookmarks/bookmark_list.html', name='bookmark_list', takes_context=True)
|
@register.inclusion_tag('bookmarks/bookmark_list.html', name='bookmark_list', takes_context=True)
|
||||||
def bookmark_list(context, bookmarks: Page, return_url: str):
|
def bookmark_list(context, bookmarks: Page, return_url: str):
|
||||||
return {
|
return {
|
||||||
|
'request': context['request'],
|
||||||
'bookmarks': bookmarks,
|
'bookmarks': bookmarks,
|
||||||
'return_url': return_url
|
'return_url': return_url
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
|
from bookmarks import utils
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,3 +45,17 @@ def first_char(text):
|
||||||
@register.filter(name='remaining_chars')
|
@register.filter(name='remaining_chars')
|
||||||
def remaining_chars(text, index):
|
def remaining_chars(text, index):
|
||||||
return text[index:]
|
return text[index:]
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='humanize_absolute_date')
|
||||||
|
def humanize_absolute_date(value):
|
||||||
|
if value in (None, ''):
|
||||||
|
return ''
|
||||||
|
return utils.humanize_absolute_date(value)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='humanize_relative_date')
|
||||||
|
def humanize_relative_date(value):
|
||||||
|
if value in (None, ''):
|
||||||
|
return ''
|
||||||
|
return utils.humanize_relative_date(value)
|
||||||
|
|
51
bookmarks/tests/test_bookmarks_list_tag.py
Normal file
51
bookmarks/tests/test_bookmarks_list_tag.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.template import Template, RequestContext
|
||||||
|
from django.test import TestCase, RequestFactory
|
||||||
|
from django.utils import timezone, formats
|
||||||
|
|
||||||
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
from bookmarks.models import UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||||
|
|
||||||
|
def render_template(self, bookmarks) -> str:
|
||||||
|
rf = RequestFactory()
|
||||||
|
request = rf.get('/test')
|
||||||
|
request.user = self.get_or_create_test_user()
|
||||||
|
paginator = Paginator(bookmarks, 10)
|
||||||
|
page = paginator.page(1)
|
||||||
|
|
||||||
|
context = RequestContext(request, {'bookmarks': page, 'return_url': '/test'})
|
||||||
|
template_to_render = Template(
|
||||||
|
'{% load bookmarks %}'
|
||||||
|
'{% bookmark_list bookmarks return_url %}'
|
||||||
|
)
|
||||||
|
return template_to_render.render(context)
|
||||||
|
|
||||||
|
def setup_date_format_test(self, date_display_setting):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
||||||
|
bookmark.save()
|
||||||
|
user = self.get_or_create_test_user()
|
||||||
|
user.profile.bookmark_date_display = date_display_setting
|
||||||
|
user.profile.save()
|
||||||
|
return bookmark
|
||||||
|
|
||||||
|
def test_should_respect_absolute_date_setting(self):
|
||||||
|
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE)
|
||||||
|
html = self.render_template([bookmark])
|
||||||
|
formatted_date = formats.date_format(bookmark.date_added, 'SHORT_DATE_FORMAT')
|
||||||
|
|
||||||
|
self.assertInHTML(f'''
|
||||||
|
<span class="text-gray text-sm">{formatted_date}</span>
|
||||||
|
''', html)
|
||||||
|
|
||||||
|
def test_should_respect_relative_date_setting(self):
|
||||||
|
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE)
|
||||||
|
html = self.render_template([bookmark])
|
||||||
|
|
||||||
|
self.assertInHTML('''
|
||||||
|
<span class="text-gray text-sm">1 week ago</span>
|
||||||
|
''', html)
|
47
bookmarks/tests/test_utils.py
Normal file
47
bookmarks/tests/test_utils.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bookmarks.utils import humanize_absolute_date, humanize_relative_date
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_humanize_absolute_date(self):
|
||||||
|
test_cases = [
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2023, 1, 1), '01/01/2021'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 2, 1), '01/01/2021'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 8), '01/01/2021'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 7), 'Friday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 7, 23, 59), 'Friday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 3), 'Friday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 2), 'Yesterday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 2, 23, 59), 'Yesterday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 1), 'Today'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for test_case in test_cases:
|
||||||
|
result = humanize_absolute_date(test_case[0], test_case[1])
|
||||||
|
self.assertEqual(test_case[2], result)
|
||||||
|
|
||||||
|
def test_humanize_relative_date(self):
|
||||||
|
test_cases = [
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2022, 1, 1), '1 year ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2022, 12, 31), '1 year ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2023, 1, 1), '2 years ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2023, 12, 31), '2 years ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 12, 31), '11 months ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 2, 1), '1 month ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 31), '4 weeks ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 14), '1 week ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 8), '1 week ago'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 7), 'Friday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 7, 23, 59), 'Friday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 3), 'Friday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 2), 'Yesterday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 2, 23, 59), 'Yesterday'),
|
||||||
|
(timezone.datetime(2021, 1, 1), timezone.datetime(2021, 1, 1), 'Today'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for test_case in test_cases:
|
||||||
|
result = humanize_relative_date(test_case[0], test_case[1])
|
||||||
|
self.assertEqual(test_case[2], result)
|
|
@ -1,2 +1,55 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from django.template.defaultfilters import pluralize
|
||||||
|
from django.utils import timezone, formats
|
||||||
|
|
||||||
|
|
||||||
def unique(elements, key):
|
def unique(elements, key):
|
||||||
return list({key(element): element for element in elements}.values())
|
return list({key(element): element for element in elements}.values())
|
||||||
|
|
||||||
|
|
||||||
|
weekday_names = {
|
||||||
|
1: 'Monday',
|
||||||
|
2: 'Tuesday',
|
||||||
|
3: 'Wednesday',
|
||||||
|
4: 'Thursday',
|
||||||
|
5: 'Friday',
|
||||||
|
6: 'Saturday',
|
||||||
|
7: 'Sunday',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def humanize_absolute_date(value: datetime, now=timezone.now()):
|
||||||
|
delta = relativedelta(now, value)
|
||||||
|
yesterday = now - relativedelta(days=1)
|
||||||
|
|
||||||
|
is_older_than_a_week = delta.years > 0 or delta.months > 0 or delta.weeks > 0
|
||||||
|
|
||||||
|
if is_older_than_a_week:
|
||||||
|
return formats.date_format(value, 'SHORT_DATE_FORMAT')
|
||||||
|
elif value.day == now.day:
|
||||||
|
return 'Today'
|
||||||
|
elif value.day == yesterday.day:
|
||||||
|
return 'Yesterday'
|
||||||
|
else:
|
||||||
|
return weekday_names[value.isoweekday()]
|
||||||
|
|
||||||
|
|
||||||
|
def humanize_relative_date(value: datetime, now: datetime = timezone.now()):
|
||||||
|
delta = relativedelta(now, value)
|
||||||
|
|
||||||
|
if delta.years > 0:
|
||||||
|
return f'{delta.years} year{pluralize(delta.years)} ago'
|
||||||
|
elif delta.months > 0:
|
||||||
|
return f'{delta.months} month{pluralize(delta.months)} ago'
|
||||||
|
elif delta.weeks > 0:
|
||||||
|
return f'{delta.weeks} week{pluralize(delta.weeks)} ago'
|
||||||
|
else:
|
||||||
|
yesterday = now - relativedelta(days=1)
|
||||||
|
if value.day == now.day:
|
||||||
|
return 'Today'
|
||||||
|
elif value.day == yesterday.day:
|
||||||
|
return 'Yesterday'
|
||||||
|
else:
|
||||||
|
return weekday_names[value.isoweekday()]
|
||||||
|
|
|
@ -11,6 +11,7 @@ django-widget-tweaks==1.4.5
|
||||||
djangorestframework==3.11.2
|
djangorestframework==3.11.2
|
||||||
idna==2.8
|
idna==2.8
|
||||||
pyparsing==2.4.7
|
pyparsing==2.4.7
|
||||||
|
python-dateutil==2.8.1
|
||||||
pytz==2019.1
|
pytz==2019.1
|
||||||
requests==2.22.0
|
requests==2.22.0
|
||||||
soupsieve==1.9.2
|
soupsieve==1.9.2
|
||||||
|
|
|
@ -15,6 +15,7 @@ djangorestframework==3.11.2
|
||||||
idna==2.8
|
idna==2.8
|
||||||
libsass==0.19.2
|
libsass==0.19.2
|
||||||
pyparsing==2.4.7
|
pyparsing==2.4.7
|
||||||
|
python-dateutil==2.8.1
|
||||||
pytz==2019.1
|
pytz==2019.1
|
||||||
rcssmin==1.0.6
|
rcssmin==1.0.6
|
||||||
requests==2.22.0
|
requests==2.22.0
|
||||||
|
|
|
@ -52,6 +52,7 @@ MIDDLEWARE = [
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'siteroot.urls'
|
ROOT_URLCONF = 'siteroot.urls'
|
||||||
|
|
Loading…
Add table
Reference in a new issue