From f92c3dd4032767fd1b1e44a6e60eed7f309dc0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Sat, 14 May 2022 09:46:51 +0200 Subject: [PATCH] Make Internet Archive integration opt-in (#250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make web archive integration opt-in * Add toast message about web archive integration opt-in * Improve wording for web archive setting * Add toast admin * Fix toast clear button visited styles * Add test for redirect * Improve wording * Ensure redirects to same domain * Improve wording * Fix snapshot test Co-authored-by: Sascha Ißbrücker --- bookmarks/admin.py | 11 +- bookmarks/context_processors.py | 12 ++ ...011_userprofile_web_archive_integration.py | 18 +++ bookmarks/migrations/0012_toast.py | 26 +++++ .../0013_web_archive_optin_toast.py | 30 +++++ bookmarks/models.py | 17 ++- bookmarks/services/bookmarks.py | 4 +- bookmarks/services/importer.py | 2 +- bookmarks/services/tasks.py | 37 +++--- bookmarks/signals.py | 2 +- bookmarks/styles/base.scss | 12 ++ .../templates/bookmarks/bookmark_list.html | 4 +- bookmarks/templates/bookmarks/layout.html | 36 ++++-- bookmarks/templates/settings/general.html | 21 ++++ bookmarks/tests/test_bookmarks_list_tag.py | 2 +- bookmarks/tests/test_bookmarks_service.py | 4 +- bookmarks/tests/test_bookmarks_tasks.py | 68 +++++++---- bookmarks/tests/test_importer.py | 2 +- bookmarks/tests/test_settings_general_view.py | 2 + bookmarks/tests/test_signals.py | 2 +- bookmarks/tests/test_toasts_view.py | 108 ++++++++++++++++++ bookmarks/urls.py | 2 + bookmarks/views/__init__.py | 1 + bookmarks/views/toasts.py | 19 +++ siteroot/settings/base.py | 1 + 25 files changed, 376 insertions(+), 67 deletions(-) create mode 100644 bookmarks/context_processors.py create mode 100644 bookmarks/migrations/0011_userprofile_web_archive_integration.py create mode 100644 bookmarks/migrations/0012_toast.py create mode 100644 bookmarks/migrations/0013_web_archive_optin_toast.py create mode 100644 bookmarks/tests/test_toasts_view.py create mode 100644 bookmarks/views/toasts.py diff --git a/bookmarks/admin.py b/bookmarks/admin.py index 0133cf7..42d87fa 100644 --- a/bookmarks/admin.py +++ b/bookmarks/admin.py @@ -7,7 +7,7 @@ from django.utils.translation import ngettext, gettext from rest_framework.authtoken.admin import TokenAdmin from rest_framework.authtoken.models import TokenProxy -from bookmarks.models import Bookmark, Tag, UserProfile +from bookmarks.models import Bookmark, Tag, UserProfile, Toast from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark @@ -58,7 +58,7 @@ class AdminTag(admin.ModelAdmin): def bookmarks_count(self, obj): return obj.bookmarks_count - + bookmarks_count.admin_order_field = 'bookmarks_count' def delete_unused_tags(self, request, queryset: QuerySet): @@ -95,8 +95,15 @@ class AdminCustomUser(UserAdmin): return super(AdminCustomUser, self).get_inline_instances(request, obj) +class AdminToast(admin.ModelAdmin): + list_display = ('key', 'message', 'owner', 'acknowledged') + search_fields = ('key', 'message') + list_filter = ('owner__username',) + + linkding_admin_site = LinkdingAdminSite() linkding_admin_site.register(Bookmark, AdminBookmark) linkding_admin_site.register(Tag, AdminTag) linkding_admin_site.register(User, AdminCustomUser) linkding_admin_site.register(TokenProxy, TokenAdmin) +linkding_admin_site.register(Toast, AdminToast) diff --git a/bookmarks/context_processors.py b/bookmarks/context_processors.py new file mode 100644 index 0000000..1773b6e --- /dev/null +++ b/bookmarks/context_processors.py @@ -0,0 +1,12 @@ +from bookmarks.models import Toast + + +def toasts(request): + user = request.user if hasattr(request, 'user') else None + toast_messages = Toast.objects.filter(owner=user, acknowledged=False) if user and user.is_authenticated else [] + has_toasts = len(toast_messages) > 0 + + return { + 'has_toasts': has_toasts, + 'toast_messages': toast_messages, + } diff --git a/bookmarks/migrations/0011_userprofile_web_archive_integration.py b/bookmarks/migrations/0011_userprofile_web_archive_integration.py new file mode 100644 index 0000000..309e4b0 --- /dev/null +++ b/bookmarks/migrations/0011_userprofile_web_archive_integration.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2022-01-08 12:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookmarks', '0010_userprofile_bookmark_link_target'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='web_archive_integration', + field=models.CharField(choices=[('disabled', 'Disabled'), ('enabled', 'Enabled')], default='disabled', max_length=10), + ), + ] diff --git a/bookmarks/migrations/0012_toast.py b/bookmarks/migrations/0012_toast.py new file mode 100644 index 0000000..b4136c0 --- /dev/null +++ b/bookmarks/migrations/0012_toast.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.6 on 2022-01-08 19:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('bookmarks', '0011_userprofile_web_archive_integration'), + ] + + operations = [ + migrations.CreateModel( + name='Toast', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=50)), + ('message', models.TextField()), + ('acknowledged', models.BooleanField(default=False)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/bookmarks/migrations/0013_web_archive_optin_toast.py b/bookmarks/migrations/0013_web_archive_optin_toast.py new file mode 100644 index 0000000..c079a19 --- /dev/null +++ b/bookmarks/migrations/0013_web_archive_optin_toast.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.6 on 2022-01-08 19:27 + +from django.db import migrations +from django.contrib.auth import get_user_model + +from bookmarks.models import Toast + +User = get_user_model() + + +def forwards(apps, schema_editor): + for user in User.objects.all(): + toast = Toast(key='web_archive_opt_in_hint', + message='The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.', + owner=user) + toast.save() + + +def reverse(apps, schema_editor): + Toast.objects.filter(key='web_archive_opt_in_hint').delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('bookmarks', '0012_toast'), + ] + + operations = [ + migrations.RunPython(forwards, reverse), + ] diff --git a/bookmarks/models.py b/bookmarks/models.py index 764c492..fe64b8b 100644 --- a/bookmarks/models.py +++ b/bookmarks/models.py @@ -128,18 +128,26 @@ class UserProfile(models.Model): (BOOKMARK_LINK_TARGET_BLANK, 'New page'), (BOOKMARK_LINK_TARGET_SELF, 'Same page'), ] + WEB_ARCHIVE_INTEGRATION_DISABLED = 'disabled' + WEB_ARCHIVE_INTEGRATION_ENABLED = 'enabled' + WEB_ARCHIVE_INTEGRATION_CHOICES = [ + (WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'), + (WEB_ARCHIVE_INTEGRATION_ENABLED, 'Enabled'), + ] 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) bookmark_link_target = models.CharField(max_length=10, choices=BOOKMARK_LINK_TARGET_CHOICES, blank=False, default=BOOKMARK_LINK_TARGET_BLANK) + web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False, + default=WEB_ARCHIVE_INTEGRATION_DISABLED) class UserProfileForm(forms.ModelForm): class Meta: model = UserProfile - fields = ['theme', 'bookmark_date_display', 'bookmark_link_target'] + fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration'] @receiver(post_save, sender=get_user_model()) @@ -151,3 +159,10 @@ def create_user_profile(sender, instance, created, **kwargs): @receiver(post_save, sender=get_user_model()) def save_user_profile(sender, instance, **kwargs): instance.profile.save() + + +class Toast(models.Model): + key = models.CharField(max_length=50) + message = models.TextField() + acknowledged = models.BooleanField(default=False) + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) diff --git a/bookmarks/services/bookmarks.py b/bookmarks/services/bookmarks.py index 3408fd5..ade5eb8 100644 --- a/bookmarks/services/bookmarks.py +++ b/bookmarks/services/bookmarks.py @@ -29,7 +29,7 @@ def create_bookmark(bookmark: Bookmark, tag_string: str, current_user: User): _update_bookmark_tags(bookmark, tag_string, current_user) bookmark.save() # Create snapshot on web archive - tasks.create_web_archive_snapshot(bookmark.id, False) + tasks.create_web_archive_snapshot(current_user, bookmark, False) return bookmark @@ -47,7 +47,7 @@ def update_bookmark(bookmark: Bookmark, tag_string, current_user: User): bookmark.save() # Update web archive snapshot, if URL changed if has_url_changed: - tasks.create_web_archive_snapshot(bookmark.id, True) + tasks.create_web_archive_snapshot(current_user, bookmark, True) return bookmark diff --git a/bookmarks/services/importer.py b/bookmarks/services/importer.py index 7fb0bd9..b72f32c 100644 --- a/bookmarks/services/importer.py +++ b/bookmarks/services/importer.py @@ -40,7 +40,7 @@ def import_netscape_html(html: str, user: User): result.failed = result.failed + 1 # Create snapshots for newly imported bookmarks - tasks.schedule_bookmarks_without_snapshots(user.id) + tasks.schedule_bookmarks_without_snapshots(user) return result diff --git a/bookmarks/services/tasks.py b/bookmarks/services/tasks.py index eb7347d..5c8300d 100644 --- a/bookmarks/services/tasks.py +++ b/bookmarks/services/tasks.py @@ -4,30 +4,29 @@ import waybackpy from background_task import background from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.auth.models import User from waybackpy.exceptions import WaybackError -from bookmarks.models import Bookmark +from bookmarks.models import Bookmark, UserProfile logger = logging.getLogger(__name__) -def when_background_tasks_enabled(fn): - def wrapper(*args, **kwargs): - if settings.LD_DISABLE_BACKGROUND_TASKS: - return - return fn(*args, **kwargs) +def is_web_archive_integration_active(user: User) -> bool: + background_tasks_enabled = not settings.LD_DISABLE_BACKGROUND_TASKS + web_archive_integration_enabled = \ + user.profile.web_archive_integration == UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED - # Expose attributes from wrapped TaskProxy function - attrs = vars(fn) - for key, value in attrs.items(): - setattr(wrapper, key, value) - - return wrapper + return background_tasks_enabled and web_archive_integration_enabled + + +def create_web_archive_snapshot(user: User, bookmark: Bookmark, force_update: bool): + if is_web_archive_integration_active(user): + _create_web_archive_snapshot_task(bookmark.id, force_update) -@when_background_tasks_enabled @background() -def create_web_archive_snapshot(bookmark_id: int, force_update: bool): +def _create_web_archive_snapshot_task(bookmark_id: int, force_update: bool): try: bookmark = Bookmark.objects.get(id=bookmark_id) except Bookmark.DoesNotExist: @@ -52,11 +51,15 @@ def create_web_archive_snapshot(bookmark_id: int, force_update: bool): logger.debug(f'Successfully created web archive link for bookmark: {bookmark}...') -@when_background_tasks_enabled +def schedule_bookmarks_without_snapshots(user: User): + if is_web_archive_integration_active(user): + _schedule_bookmarks_without_snapshots_task(user.id) + + @background() -def schedule_bookmarks_without_snapshots(user_id: int): +def _schedule_bookmarks_without_snapshots_task(user_id: int): user = get_user_model().objects.get(id=user_id) bookmarks_without_snapshots = Bookmark.objects.filter(web_archive_snapshot_url__exact='', owner=user) for bookmark in bookmarks_without_snapshots: - create_web_archive_snapshot(bookmark.id, False) + _create_web_archive_snapshot_task(bookmark.id, False) diff --git a/bookmarks/signals.py b/bookmarks/signals.py index 602ef4f..dafb1b9 100644 --- a/bookmarks/signals.py +++ b/bookmarks/signals.py @@ -5,4 +5,4 @@ from bookmarks.services import tasks @receiver(user_logged_in) def user_logged_in(sender, request, user, **kwargs): - tasks.schedule_bookmarks_without_snapshots(user.id) + tasks.schedule_bookmarks_without_snapshots(user) diff --git a/bookmarks/styles/base.scss b/bookmarks/styles/base.scss index f7f1a96..5086c50 100644 --- a/bookmarks/styles/base.scss +++ b/bookmarks/styles/base.scss @@ -11,6 +11,18 @@ header { margin-bottom: 40px; } +header .toasts { + margin-bottom: 20px; + + .toast { + margin-bottom: 0.4rem; + } + + .toast a.btn-clear:visited { + color: currentColor; + } +} + .navbar { .navbar-brand { diff --git a/bookmarks/templates/bookmarks/bookmark_list.html b/bookmarks/templates/bookmarks/bookmark_list.html index 829aa78..7777e38 100644 --- a/bookmarks/templates/bookmarks/bookmark_list.html +++ b/bookmarks/templates/bookmarks/bookmark_list.html @@ -30,7 +30,7 @@ {% if bookmark.web_archive_snapshot_url %} + title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}" rel="noopener"> {% endif %} {{ bookmark.date_added|humanize_relative_date }} {% if bookmark.web_archive_snapshot_url %} @@ -44,7 +44,7 @@ {% if bookmark.web_archive_snapshot_url %} + title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}" rel="noopener"> {% endif %} {{ bookmark.date_added|humanize_absolute_date }} {% if bookmark.web_archive_snapshot_url %} diff --git a/bookmarks/templates/bookmarks/layout.html b/bookmarks/templates/bookmarks/layout.html index c0e5950..e6ce7a7 100644 --- a/bookmarks/templates/bookmarks/layout.html +++ b/bookmarks/templates/bookmarks/layout.html @@ -27,19 +27,31 @@ {% endif %} -