From a02338cdec73f5e960007c744d30aa366e77e25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Wed, 24 Feb 2021 03:36:27 +0100 Subject: [PATCH] Improve and promote admin panel (#76) * Improve and promote admin panel (#76) * Customize admin panel texts (#76) * Improve settings structure (#76) * Improve admin list consistency (#76) * Fix redirect URLs (#76) * Add admin tooltip (#76) --- bookmarks/admin.py | 89 +++++++++++++++--- bookmarks/styles/base.scss | 5 + bookmarks/styles/settings.scss | 5 + .../templates/bookmarks/empty_bookmarks.html | 4 +- bookmarks/templates/settings/api.html | 25 +++++ bookmarks/templates/settings/data.html | 54 +++++++++++ bookmarks/templates/settings/index.html | 91 ------------------- .../templates/settings/integrations.html | 26 ++++++ bookmarks/templates/settings/nav.html | 23 +++++ bookmarks/urls.py | 5 +- bookmarks/views/settings.py | 24 +++-- siteroot/urls.py | 5 +- 12 files changed, 243 insertions(+), 113 deletions(-) create mode 100644 bookmarks/templates/settings/api.html create mode 100644 bookmarks/templates/settings/data.html delete mode 100644 bookmarks/templates/settings/index.html create mode 100644 bookmarks/templates/settings/integrations.html create mode 100644 bookmarks/templates/settings/nav.html diff --git a/bookmarks/admin.py b/bookmarks/admin.py index 7c8dbc0..c351998 100644 --- a/bookmarks/admin.py +++ b/bookmarks/admin.py @@ -1,17 +1,84 @@ -from django.contrib import admin +from django.contrib import admin, messages +from django.contrib.admin import AdminSite +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import User +from django.db.models import Count, QuerySet +from django.utils.translation import ngettext, gettext +from rest_framework.authtoken.admin import TokenAdmin +from rest_framework.authtoken.models import Token from bookmarks.models import Bookmark, Tag +from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark + + +class LinkdingAdminSite(AdminSite): + site_header = 'linkding administration' + site_title = 'linkding Admin' + -@admin.register(Bookmark) class AdminBookmark(admin.ModelAdmin): - list_display = ('title', 'url', 'date_added') - search_fields = ('title', 'url', 'tags__name') - list_filter = ('tags',) - ordering = ('-date_added', ) + list_display = ('resolved_title', 'url', 'is_archived', 'owner', 'date_added') + search_fields = ('title', 'description', 'website_title', 'website_description', 'url', 'tags__name') + list_filter = ('owner__username', 'is_archived', 'tags',) + ordering = ('-date_added',) + actions = ['archive_selected_bookmarks', 'unarchive_selected_bookmarks'] + + def archive_selected_bookmarks(self, request, queryset: QuerySet): + for bookmark in queryset: + archive_bookmark(bookmark) + bookmarks_count = queryset.count() + self.message_user(request, ngettext( + '%d bookmark was successfully archived.', + '%d bookmarks were successfully archived.', + bookmarks_count, + ) % bookmarks_count, messages.SUCCESS) + + def unarchive_selected_bookmarks(self, request, queryset: QuerySet): + for bookmark in queryset: + unarchive_bookmark(bookmark) + bookmarks_count = queryset.count() + self.message_user(request, ngettext( + '%d bookmark was successfully unarchived.', + '%d bookmarks were successfully unarchived.', + bookmarks_count, + ) % bookmarks_count, messages.SUCCESS) + -@admin.register(Tag) class AdminTag(admin.ModelAdmin): - list_display = ('name', 'date_added', 'owner') - search_fields = ('name', 'owner__username') - list_filter = ('owner__username', ) - ordering = ('-date_added', ) + list_display = ('name', 'bookmarks_count', 'owner', 'date_added') + search_fields = ('name', 'owner__username') + list_filter = ('owner__username',) + ordering = ('-date_added',) + actions = ['delete_unused_tags'] + + def get_queryset(self, request): + queryset = super().get_queryset(request) + queryset = queryset.annotate(bookmarks_count=Count("bookmark")) + return queryset + + def bookmarks_count(self, obj): + return obj.bookmarks_count + + def delete_unused_tags(self, request, queryset: QuerySet): + unused_tags = queryset.filter(bookmark__isnull=True) + unused_tags_count = unused_tags.count() + for tag in unused_tags: + tag.delete() + + if unused_tags_count > 0: + self.message_user(request, ngettext( + '%d unused tag was successfully deleted.', + '%d unused tags were successfully deleted.', + unused_tags_count, + ) % unused_tags_count, messages.SUCCESS) + else: + self.message_user(request, gettext( + 'There were no unused tags in the selection', + ), messages.SUCCESS) + + +linkding_admin_site = LinkdingAdminSite() +linkding_admin_site.register(Bookmark, AdminBookmark) +linkding_admin_site.register(Tag, AdminTag) +linkding_admin_site.register(User, UserAdmin) +linkding_admin_site.register(Token, TokenAdmin) diff --git a/bookmarks/styles/base.scss b/bookmarks/styles/base.scss index dabb795..8c2388f 100644 --- a/bookmarks/styles/base.scss +++ b/bookmarks/styles/base.scss @@ -52,4 +52,9 @@ h2 { // Remove left padding from first pagination link .pagination .page-item:first-child a { padding-left: 0; +} + +// Override border color for tab block +.tab-block { + border-bottom: solid 1px $border-color; } \ No newline at end of file diff --git a/bookmarks/styles/settings.scss b/bookmarks/styles/settings.scss index 11415de..b60bb96 100644 --- a/bookmarks/styles/settings.scss +++ b/bookmarks/styles/settings.scss @@ -1,6 +1,11 @@ .settings-page { section.content-area { margin-bottom: 2rem; + + h2 { + font-size: 1.0rem; + margin-bottom: 0.8rem; + } } .input-group > input[type=submit] { diff --git a/bookmarks/templates/bookmarks/empty_bookmarks.html b/bookmarks/templates/bookmarks/empty_bookmarks.html index aed6895..376b1fa 100644 --- a/bookmarks/templates/bookmarks/empty_bookmarks.html +++ b/bookmarks/templates/bookmarks/empty_bookmarks.html @@ -2,7 +2,7 @@

You have no bookmarks yet

You can get started by adding bookmarks, - importing your existing bookmarks or configuring the - bookmarklet. + importing your existing bookmarks or configuring the + bookmarklet.

diff --git a/bookmarks/templates/settings/api.html b/bookmarks/templates/settings/api.html new file mode 100644 index 0000000..e8fca55 --- /dev/null +++ b/bookmarks/templates/settings/api.html @@ -0,0 +1,25 @@ +{% extends "bookmarks/layout.html" %} + +{% block content %} +
+ + {% include 'settings/nav.html' %} + +
+

API Token

+

The following token can be used to authenticate 3rd-party applications against the REST API:

+
+
+
+ +
+
+
+

Please treat this token as you would any other credential. Any party with access to this + token can access and manage all your bookmarks.

+

If you think that a token was compromised you can revoke (delete) it in the admin panel. After deleting the token, a new one will be generated when you reload this settings page.

+
+ +
+ +{% endblock %} diff --git a/bookmarks/templates/settings/data.html b/bookmarks/templates/settings/data.html new file mode 100644 index 0000000..4da109c --- /dev/null +++ b/bookmarks/templates/settings/data.html @@ -0,0 +1,54 @@ +{% extends "bookmarks/layout.html" %} + +{% block content %} +
+ + {% include 'settings/nav.html' %} + + {# Import section #} +
+

Import

+

Import bookmarks and tags in the Netscape HTML format. This will execute a sync where new bookmarks are + added and existing ones are updated.

+
+ {% csrf_token %} +
+
+ + +
+ {% if import_success_message %} +
+

+ {{ import_success_message }} +

+
+ {% endif %} + {% if import_errors_message %} +
+

+ {{ import_errors_message }} +

+
+ {% endif %} +
+
+
+ + {# Export section #} +
+

Export

+

Export all bookmarks in Netscape HTML format.

+ Download (.html) + {% if export_error %} +
+

+ {{ export_error }} +

+
+ {% endif %} +
+ +
+ +{% endblock %} diff --git a/bookmarks/templates/settings/index.html b/bookmarks/templates/settings/index.html deleted file mode 100644 index 0f281a8..0000000 --- a/bookmarks/templates/settings/index.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends "bookmarks/layout.html" %} - -{% block content %} -
- - {# Import section #} -
-
-

Import

-
-

Import bookmarks and tags in the Netscape HTML format. This will execute a sync where new bookmarks are - added and existing ones are updated.

-
- {% csrf_token %} -
-
- - -
- {% if import_success_message %} -
-

- {{ import_success_message }} -

-
- {% endif %} - {% if import_errors_message %} -
-

- {{ import_errors_message }} -

-
- {% endif %} -
-
-
- - {# Export section #} -
-
-

Export

-
-

Export all bookmarks in Netscape HTML format.

- Download (.html) - {% if export_error %} -
-

- {{ export_error }} -

-
- {% endif %} -
- - {# Integrations section #} -
- -

The bookmarklet is a quick way to add new bookmarks without opening the linkding application - first. Here's how it works:

-
    -
  • Drag the bookmarklet below into your browsers bookmark bar / toolbar
  • -
  • Open the website that you want to bookmark
  • -
  • Click the bookmarklet in your browsers toolbar
  • -
  • linkding opens in a new window or tab and allows you to add a bookmark for the site
  • -
  • After saving the bookmark the linkding window closes and you are back on your website
  • -
-

Drag the following bookmarklet to your browsers toolbar:

- 📎 Add bookmark -
- - {# API token section #} -
-
-

API Token

-
-

The following token can be used to authenticate 3rd-party applications against the REST API:

-
-
-
- -
-
-
-

Please treat this token as you would any other credential. Any party with access to this token can access and manage all your bookmarks.

-
- -
- -{% endblock %} diff --git a/bookmarks/templates/settings/integrations.html b/bookmarks/templates/settings/integrations.html new file mode 100644 index 0000000..7142531 --- /dev/null +++ b/bookmarks/templates/settings/integrations.html @@ -0,0 +1,26 @@ +{% extends "bookmarks/layout.html" %} + +{% block content %} +
+ + {% include 'settings/nav.html' %} + + {# Integrations section #} +
+

Bookmarklet

+

The bookmarklet is a quick way to add new bookmarks without opening the linkding application + first. Here's how it works:

+
    +
  • Drag the bookmarklet below into your browsers bookmark bar / toolbar
  • +
  • Open the website that you want to bookmark
  • +
  • Click the bookmarklet in your browsers toolbar
  • +
  • linkding opens in a new window or tab and allows you to add a bookmark for the site
  • +
  • After saving the bookmark the linkding window closes and you are back on your website
  • +
+

Drag the following bookmarklet to your browsers toolbar:

+ 📎 Add bookmark +
+ +
+{% endblock %} diff --git a/bookmarks/templates/settings/nav.html b/bookmarks/templates/settings/nav.html new file mode 100644 index 0000000..d298a3f --- /dev/null +++ b/bookmarks/templates/settings/nav.html @@ -0,0 +1,23 @@ +{% url 'bookmarks:settings.index' as index_url %} +{% url 'bookmarks:settings.data' as data_url %} +{% url 'bookmarks:settings.integrations' as integrations_url %} +{% url 'bookmarks:settings.api' as api_url %} + + +
\ No newline at end of file diff --git a/bookmarks/urls.py b/bookmarks/urls.py index 2821905..6228b47 100644 --- a/bookmarks/urls.py +++ b/bookmarks/urls.py @@ -19,7 +19,10 @@ urlpatterns = [ path('bookmarks//archive', views.bookmarks.archive, name='archive'), path('bookmarks//unarchive', views.bookmarks.unarchive, name='unarchive'), # Settings - path('settings', views.settings.index, name='settings.index'), + path('settings', views.settings.data, name='settings.index'), + path('settings/data', views.settings.data, name='settings.data'), + path('settings/integrations', views.settings.integrations, name='settings.integrations'), + path('settings/api', views.settings.api, name='settings.api'), path('settings/import', views.settings.bookmark_import, name='settings.import'), path('settings/export', views.settings.bookmark_export, name='settings.export'), # API diff --git a/bookmarks/views/settings.py b/bookmarks/views/settings.py index c989e00..95657d3 100644 --- a/bookmarks/views/settings.py +++ b/bookmarks/views/settings.py @@ -15,15 +15,27 @@ logger = logging.getLogger(__name__) @login_required -def index(request): +def data(request): import_success_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_success') import_errors_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_errors') - application_url = request.build_absolute_uri("/bookmarks/new") - api_token = Token.objects.get_or_create(user=request.user)[0] - return render(request, 'settings/index.html', { + return render(request, 'settings/data.html', { 'import_success_message': import_success_message, 'import_errors_message': import_errors_message, + }) + + +@login_required +def integrations(request): + application_url = request.build_absolute_uri("/bookmarks/new") + return render(request, 'settings/integrations.html', { 'application_url': application_url, + }) + + +@login_required +def api(request): + api_token = Token.objects.get_or_create(user=request.user)[0] + return render(request, 'settings/api.html', { 'api_token': api_token.key }) @@ -49,7 +61,7 @@ def bookmark_import(request): messages.error(request, 'An error occurred during bookmark import.', 'bookmark_import_errors') pass - return HttpResponseRedirect(reverse('bookmarks:settings.index')) + return HttpResponseRedirect(reverse('bookmarks:settings.data')) @login_required @@ -65,7 +77,7 @@ def bookmark_export(request): return response except: - return render(request, 'settings/index.html', { + return render(request, 'settings/data.html', { 'export_error': 'An error occurred during bookmark export.' }) diff --git a/siteroot/urls.py b/siteroot/urls.py index a0e9f09..9c00455 100644 --- a/siteroot/urls.py +++ b/siteroot/urls.py @@ -13,13 +13,14 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.contrib import admin from django.contrib.auth import views as auth_views from django.urls import path, include + +from bookmarks.admin import linkding_admin_site from .settings import ALLOW_REGISTRATION urlpatterns = [ - path('admin/', admin.site.urls), + path('admin/', linkding_admin_site.urls), path('login/', auth_views.LoginView.as_view(redirect_authenticated_user=True, extra_context=dict(allow_registration=ALLOW_REGISTRATION)), name='login'),