diff --git a/bookmarks/migrations/0008_userprofile_bookmark_date_display.py b/bookmarks/migrations/0008_userprofile_bookmark_date_display.py new file mode 100644 index 0000000..f27ce49 --- /dev/null +++ b/bookmarks/migrations/0008_userprofile_bookmark_date_display.py @@ -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), + ), + ] diff --git a/bookmarks/models.py b/bookmarks/models.py index 7bbb535..1fd079c 100644 --- a/bookmarks/models.py +++ b/bookmarks/models.py @@ -107,14 +107,24 @@ class UserProfile(models.Model): (THEME_LIGHT, 'Light'), (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) 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 Meta: model = UserProfile - fields = ['theme'] + fields = ['theme', 'bookmark_date_display'] @receiver(post_save, sender=get_user_model()) diff --git a/bookmarks/queries.py b/bookmarks/queries.py index b36842b..bd4ab12 100644 --- a/bookmarks/queries.py +++ b/bookmarks/queries.py @@ -55,8 +55,8 @@ def _base_bookmarks_query(user: User, query_string: str) -> QuerySet: tags__name__iexact=tag_name ) - # Sort by modification date - query_set = query_set.order_by('-date_modified') + # Sort by date added + query_set = query_set.order_by('-date_added') return query_set diff --git a/bookmarks/styles/bookmarks.scss b/bookmarks/styles/bookmarks.scss index 73d4975..1614546 100644 --- a/bookmarks/styles/bookmarks.scss +++ b/bookmarks/styles/bookmarks.scss @@ -50,16 +50,22 @@ ul.bookmark-list { } } + .actions > *:not(:last-child) { + margin-right: 0.1rem; + } + .actions .btn-link { color: $gray-color; - padding-left: 0; - padding-right: 0; + padding: 0; + height: auto; + vertical-align: unset; + border: none; &:focus, &:hover, &: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 { max-height: 37px; border-bottom: solid 1px $border-color; -} \ No newline at end of file +} diff --git a/bookmarks/styles/util.scss b/bookmarks/styles/util.scss index 8d70b38..ad08a70 100644 --- a/bookmarks/styles/util.scss +++ b/bookmarks/styles/util.scss @@ -14,4 +14,4 @@ .text-gray-dark { color: $gray-color-dark; -} \ No newline at end of file +} diff --git a/bookmarks/templates/bookmarks/bookmark_list.html b/bookmarks/templates/bookmarks/bookmark_list.html index a41a830..d42c2e1 100644 --- a/bookmarks/templates/bookmarks/bookmark_list.html +++ b/bookmarks/templates/bookmarks/bookmark_list.html @@ -26,6 +26,14 @@ {% endif %}
+ {% if request.user.profile.bookmark_date_display == 'relative' %} + {{ bookmark.date_added|humanize_relative_date }} + | + {% endif %} + {% if request.user.profile.bookmark_date_display == 'absolute' %} + {{ bookmark.date_added|humanize_absolute_date }} + | + {% endif %} Edit {% if bookmark.is_archived %} @@ -44,4 +52,4 @@
{% pagination bookmarks %} -
\ No newline at end of file +
diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html index cf22091..0992ba8 100644 --- a/bookmarks/templates/settings/general.html +++ b/bookmarks/templates/settings/general.html @@ -15,6 +15,10 @@ {{ form.theme|add_class:"form-select col-2 col-sm-12" }} +
+ + {{ form.bookmark_date_display|add_class:"form-select col-2 col-sm-12" }} +
diff --git a/bookmarks/templatetags/bookmarks.py b/bookmarks/templatetags/bookmarks.py index 5f42f52..859df1c 100644 --- a/bookmarks/templatetags/bookmarks.py +++ b/bookmarks/templatetags/bookmarks.py @@ -53,6 +53,7 @@ def tag_cloud(context, tags: List[Tag]): @register.inclusion_tag('bookmarks/bookmark_list.html', name='bookmark_list', takes_context=True) def bookmark_list(context, bookmarks: Page, return_url: str): return { + 'request': context['request'], 'bookmarks': bookmarks, 'return_url': return_url } diff --git a/bookmarks/templatetags/shared.py b/bookmarks/templatetags/shared.py index 566ca6b..2eb13fd 100644 --- a/bookmarks/templatetags/shared.py +++ b/bookmarks/templatetags/shared.py @@ -1,5 +1,7 @@ from django import template +from bookmarks import utils + register = template.Library() @@ -43,3 +45,17 @@ def first_char(text): @register.filter(name='remaining_chars') def remaining_chars(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) diff --git a/bookmarks/tests/test_bookmarks_list_tag.py b/bookmarks/tests/test_bookmarks_list_tag.py new file mode 100644 index 0000000..f9ba9a3 --- /dev/null +++ b/bookmarks/tests/test_bookmarks_list_tag.py @@ -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''' + {formatted_date} + ''', 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(''' + 1 week ago + ''', html) diff --git a/bookmarks/tests/test_utils.py b/bookmarks/tests/test_utils.py new file mode 100644 index 0000000..6638335 --- /dev/null +++ b/bookmarks/tests/test_utils.py @@ -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) diff --git a/bookmarks/utils.py b/bookmarks/utils.py index ac87766..65a6dd4 100644 --- a/bookmarks/utils.py +++ b/bookmarks/utils.py @@ -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): 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()] diff --git a/requirements.prod.txt b/requirements.prod.txt index 60247f1..d168d33 100644 --- a/requirements.prod.txt +++ b/requirements.prod.txt @@ -11,6 +11,7 @@ django-widget-tweaks==1.4.5 djangorestframework==3.11.2 idna==2.8 pyparsing==2.4.7 +python-dateutil==2.8.1 pytz==2019.1 requests==2.22.0 soupsieve==1.9.2 diff --git a/requirements.txt b/requirements.txt index 0b3144f..ebec16e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ djangorestframework==3.11.2 idna==2.8 libsass==0.19.2 pyparsing==2.4.7 +python-dateutil==2.8.1 pytz==2019.1 rcssmin==1.0.6 requests==2.22.0 diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py index 1084f42..c99118e 100644 --- a/siteroot/settings/base.py +++ b/siteroot/settings/base.py @@ -52,6 +52,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.locale.LocaleMiddleware', ] ROOT_URLCONF = 'siteroot.urls'