Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Internet Archive integration opt-in #250

Merged
merged 10 commits into from
May 14, 2022
Merged
11 changes: 9 additions & 2 deletions bookmarks/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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


Expand Down Expand Up @@ -58,7 +58,7 @@ def get_queryset(self, request):

def bookmarks_count(self, obj):
return obj.bookmarks_count

bookmarks_count.admin_order_field = 'bookmarks_count'

def delete_unused_tags(self, request, queryset: QuerySet):
Expand Down Expand Up @@ -95,8 +95,15 @@ def get_inline_instances(self, request, obj=None):
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)
12 changes: 12 additions & 0 deletions bookmarks/context_processors.py
Original file line number Diff line number Diff line change
@@ -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,
}
18 changes: 18 additions & 0 deletions bookmarks/migrations/0011_userprofile_web_archive_integration.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
26 changes: 26 additions & 0 deletions bookmarks/migrations/0012_toast.py
Original file line number Diff line number Diff line change
@@ -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)),
],
),
]
30 changes: 30 additions & 0 deletions bookmarks/migrations/0013_web_archive_optin_toast.py
Original file line number Diff line number Diff line change
@@ -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),
]
17 changes: 16 additions & 1 deletion bookmarks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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)
4 changes: 2 additions & 2 deletions bookmarks/services/bookmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down
2 changes: 1 addition & 1 deletion bookmarks/services/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 19 additions & 16 deletions bookmarks/services/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,29 @@
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 background_tasks_enabled and web_archive_integration_enabled

return wrapper

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:
Expand All @@ -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)
2 changes: 1 addition & 1 deletion bookmarks/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

@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)
12 changes: 12 additions & 0 deletions bookmarks/styles/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions bookmarks/templates/bookmarks/bookmark_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<span class="date-label text-gray text-sm">
{% if bookmark.web_archive_snapshot_url %}
<a href="{{ bookmark.web_archive_snapshot_url }}"
title="Show snapshot on web archive" target="{{ link_target }}" rel="noopener">
title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}" rel="noopener">
{% endif %}
<span>{{ bookmark.date_added|humanize_relative_date }}</span>
{% if bookmark.web_archive_snapshot_url %}
Expand All @@ -44,7 +44,7 @@
<span class="date-label text-gray text-sm">
{% if bookmark.web_archive_snapshot_url %}
<a href="{{ bookmark.web_archive_snapshot_url }}"
title="Show snapshot on web archive" target="{{ link_target }}" rel="noopener">
title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}" rel="noopener">
{% endif %}
<span>{{ bookmark.date_added|humanize_absolute_date }}</span>
{% if bookmark.web_archive_snapshot_url %}
Expand Down
34 changes: 23 additions & 11 deletions bookmarks/templates/bookmarks/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,31 @@
{% endif %}
</head>
<body>
<header class="navbar container grid-lg">
<section class="navbar-section">
<a href="/" class="navbar-brand text-bold">
<img class="logo" src="{% static 'logo.png' %}" alt="Application logo">
<h1>linkding</h1>
</a>
</section>
{# Only show nav items menu when logged in #}
{% if request.user.is_authenticated %}
<header>
{% if has_toasts %}
<div class="toasts container grid-lg">
{% for toast in toast_messages %}
<div class="toast">
{{ toast.message }}
<a href="{% url 'bookmarks:toasts.acknowledge' toast.id %}?return_url={{ request.path | urlencode }}" class="btn btn-clear float-right"></a>
</div>
{% endfor %}
</div>
{% endif %}
<div class="navbar container grid-lg">
<section class="navbar-section">
{% include 'bookmarks/nav_menu.html' %}
<a href="/" class="navbar-brand text-bold">
<img class="logo" src="{% static 'logo.png' %}" alt="Application logo">
<h1>linkding</h1>
</a>
</section>
{% endif %}
{# Only show nav items menu when logged in #}
{% if request.user.is_authenticated %}
<section class="navbar-section">
{% include 'bookmarks/nav_menu.html' %}
</section>
{% endif %}
</div>
</header>
<div class="content container grid-lg">
{% block content %}
Expand Down
21 changes: 21 additions & 0 deletions bookmarks/templates/settings/general.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,35 @@ <h2>Profile</h2>
<div class="form-group">
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
{{ form.theme|add_class:"form-select col-2 col-sm-12" }}
<div class="form-input-hint">
Whether to use a light or dark theme, or automatically adjust the theme based on your system's settings.
</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 class="form-input-hint">
Whether to show bookmark dates as relative (how long ago), or as absolute dates. Alternatively the date can be hidden.
</div>
</div>
<div class="form-group">
<label for="{{ form.bookmark_link_target.id_for_label }}" class="form-label">Open bookmarks in</label>
{{ form.bookmark_link_target|add_class:"form-select col-2 col-sm-12" }}
<div class="form-input-hint">
Whether to open bookmarks a new page or in the same page.
</div>
</div>
<div class="form-group">
<label for="{{ form.web_archive_integration.id_for_label }}" class="form-label">Internet Archive
integration</label>
{{ form.web_archive_integration|add_class:"form-select col-2 col-sm-12" }}
<div class="form-input-hint">
Enabling this feature will automatically create snapshots of bookmarked websites on the <a
href="https://web.archive.org/" target="_blank" rel="noopener">Internet Archive Wayback
Machine</a>. This allows
to preserve, and later access, the website as it was at the point in time it was bookmarked, in
case it goes offline or its content is modified.
</div>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary mt-2">
Expand Down
2 changes: 1 addition & 1 deletion bookmarks/tests/test_bookmarks_list_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def assertWebArchiveLink(self, html: str, label_content: str, url: str, link_tar
self.assertInHTML(f'''
<span class="date-label text-gray text-sm">
<a href="{url}"
title="Show snapshot on web archive" target="{link_target}" rel="noopener">
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
<span>{label_content}</span>
<span>∞</span>
</a>
Expand Down
Loading