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:
Sascha Ißbrücker 2021-03-31 09:08:19 +02:00 committed by GitHub
parent 8dd1575dc6
commit 7a68a4abed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 226 additions and 9 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -14,4 +14,4 @@
.text-gray-dark { .text-gray-dark {
color: $gray-color-dark; color: $gray-color-dark;
} }

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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