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