Use single bookmark page template

This commit is contained in:
Sascha Ißbrücker
2026-01-01 18:36:28 +01:00
parent ffc1a69085
commit b4a5b34815
13 changed files with 139 additions and 193 deletions

View File

@@ -16,7 +16,7 @@ test:
format:
uv run black bookmarks
uv run djlint bookmarks/templates --reformat --quiet
uv run djlint bookmarks/templates --reformat --quiet --warn
npx prettier bookmarks/frontend --write
npx prettier bookmarks/styles --write

View File

@@ -1,37 +0,0 @@
{% extends "shared/layout.html" %}
{% load static %}
{% load shared %}
{% load bookmarks %}
{% block content %}
<ld-bookmark-page class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #}
<main class="main col-2" aria-labelledby="main-heading">
<div class="section-header mb-0">
<h1 id="main-heading">Archived bookmarks</h1>
<div class="header-controls">
{% bookmark_search bookmark_list.search mode='archived' %}
{% include 'bookmarks/bulk_edit/toggle.html' %}
<ld-filter-drawer-trigger>
<button class="btn ml-2">Filters</button>
</ld-filter-drawer-trigger>
</div>
</div>
<form class="bookmark-actions"
action="{{ bookmark_list.action_url|safe }}"
method="post"
autocomplete="off">
{% csrf_token %}
{% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_archive' %}
<div id="bookmark-list-container">{% include 'bookmarks/bookmark_list.html' %}</div>
</form>
</main>
{# Filters #}
<div class="side-panel col-1 hide-md">
{% include 'bookmarks/bundle_section.html' %}
{% include 'bookmarks/tag_section.html' %}
</div>
</ld-bookmark-page>
{% endblock %}
{% block overlays %}
{% include 'bookmarks/details/modal.html' %}
{% endblock %}

View File

@@ -0,0 +1,59 @@
{% extends "shared/layout.html" %}
{% load static shared bookmarks %}
{% block content %}
<ld-bookmark-page class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #}
<main class="main col-2" aria-labelledby="main-heading">
<div class="section-header {% if bookmark_list.bulk_edit_enabled %}mb-0{% endif %}">
<h1 id="main-heading">{{ bookmark_list.list_title }}</h1>
<div class="header-controls">
{% bookmark_search bookmark_list.search mode=bookmark_list.search_mode %}
{% if bookmark_list.bulk_edit_enabled %}
<button class="btn hide-sm ml-2 bulk-edit-active-toggle" title="Bulk edit">
<svg xmlns="http://www.w3.org/2000/svg"
width="20px"
height="20px"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
<path d="M16 5l3 3" />
</svg>
</button>
{% endif %}
<ld-filter-drawer-trigger>
<button class="btn ml-2">Filters</button>
</ld-filter-drawer-trigger>
</div>
</div>
<form class="bookmark-actions"
action="{{ bookmark_list.action_url|safe }}"
method="post"
autocomplete="off">
{% csrf_token %}
{% if bookmark_list.bulk_edit_enabled %}
{% include 'bookmarks/bulk_edit_bar.html' %}
{% endif %}
<div id="bookmark-list-container">{% include 'bookmarks/bookmark_list.html' %}</div>
</form>
</main>
{# Filters #}
<div class="side-panel col-1 hide-md">
{% if bundles %}
{% include 'bookmarks/bundle_section.html' %}
{% endif %}
{% if user_list %}
{% include 'bookmarks/user_section.html' %}
{% endif %}
{% include 'bookmarks/tag_section.html' %}
</div>
</ld-bookmark-page>
{% endblock %}
{% block overlays %}
{% include 'bookmarks/details/modal.html' %}
{% endblock %}

View File

@@ -1,16 +0,0 @@
<button class="btn hide-sm ml-2 bulk-edit-active-toggle" title="Bulk edit">
<svg xmlns="http://www.w3.org/2000/svg"
width="20px"
height="20px"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
<path d="M16 5l3 3" />
</svg>
</button>

View File

@@ -7,8 +7,12 @@
<i class="form-icon"></i>
</label>
<select name="bulk_action" class="form-select select-sm">
{% if not 'bulk_archive' in disable_actions %}<option value="bulk_archive">Archive</option>{% endif %}
{% if not 'bulk_unarchive' in disable_actions %}<option value="bulk_unarchive">Unarchive</option>{% endif %}
{% if not 'bulk_archive' in bookmark_list.bulk_edit_disabled_actions %}
<option value="bulk_archive">Archive</option>
{% endif %}
{% if not 'bulk_unarchive' in bookmark_list.bulk_edit_disabled_actions %}
<option value="bulk_unarchive">Unarchive</option>
{% endif %}
<option value="bulk_delete">Delete</option>
<option value="bulk_tag">Add tags</option>
<option value="bulk_untag">Remove tags</option>

View File

@@ -1,38 +0,0 @@
{% extends "shared/layout.html" %}
{% load static %}
{% load shared %}
{% load bookmarks %}
{% block title %}Bookmarks - Linkding{% endblock %}
{% block content %}
<ld-bookmark-page class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #}
<main class="main col-2" aria-labelledby="main-heading">
<div class="section-header mb-0">
<h1 id="main-heading">Bookmarks</h1>
<div class="header-controls">
{% bookmark_search bookmark_list.search %}
{% include 'bookmarks/bulk_edit/toggle.html' %}
<ld-filter-drawer-trigger>
<button class="btn ml-2">Filters</button>
</ld-filter-drawer-trigger>
</div>
</div>
<form class="bookmark-actions"
action="{{ bookmark_list.action_url|safe }}"
method="post"
autocomplete="off">
{% csrf_token %}
{% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_unarchive' %}
<div id="bookmark-list-container">{% include 'bookmarks/bookmark_list.html' %}</div>
</form>
</main>
{# Filters #}
<div class="side-panel col-1 hide-md">
{% include 'bookmarks/bundle_section.html' %}
{% include 'bookmarks/tag_section.html' %}
</div>
</ld-bookmark-page>
{% endblock %}
{% block overlays %}
{% include 'bookmarks/details/modal.html' %}
{% endblock %}

View File

@@ -1,44 +0,0 @@
{% extends "shared/layout.html" %}
{% load static %}
{% load shared %}
{% load bookmarks %}
{% block content %}
<ld-bookmark-page no-bulk-edit
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #}
<main class="main col-2" aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">Shared bookmarks</h1>
<div class="header-controls">
{% bookmark_search bookmark_list.search mode='shared' %}
<ld-filter-drawer-trigger>
<button class="btn ml-2">Filters</button>
</ld-filter-drawer-trigger>
</div>
</div>
<form class="bookmark-actions"
action="{{ bookmark_list.action_url|safe }}"
method="post"
autocomplete="off">
{% csrf_token %}
<div id="bookmark-list-container">{% include 'bookmarks/bookmark_list.html' %}</div>
</form>
</main>
{# Filters #}
<div class="side-panel col-1 hide-md">
<section aria-labelledby="user-heading">
<div class="section-header">
<h2 id="user-heading">User</h2>
</div>
<div>
{% user_select bookmark_list.search users %}
<br>
</div>
</section>
{% include 'bookmarks/tag_section.html' %}
</div>
</ld-bookmark-page>
{% endblock %}
{% block overlays %}
{% include 'bookmarks/details/modal.html' %}
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% load widget_tweaks %}
<section aria-labelledby="user-heading">
<div class="section-header">
<h2 id="user-heading">User</h2>
</div>
<div>
<ld-form data-form-reset>
<form id="user-select" action="" method="get">
{% for hidden_field in user_list.form.hidden_fields %}{{ hidden_field }}{% endfor %}
<div class="form-group">
<div class="d-flex">
{% render_field user_list.form.user class+="form-select" data-submit-on-change="" %}
<noscript>
<button type="submit" class="btn btn-link ml-2">Apply</button>
</noscript>
</div>
</div>
</form>
</ld-form>
<br>
</div>
</section>

View File

@@ -1,14 +0,0 @@
{% load widget_tweaks %}
<ld-form data-form-reset>
<form id="user-select" action="" method="get">
{% for hidden_field in form.hidden_fields %}{{ hidden_field }}{% endfor %}
<div class="form-group">
<div class="d-flex">
{% render_field form.user class+="form-select" data-submit-on-change="" %}
<noscript>
<button type="submit" class="btn btn-link ml-2">Apply</button>
</noscript>
</div>
</div>
</form>
</ld-form>

View File

@@ -1,11 +1,8 @@
from typing import List
from django import template
from bookmarks.models import (
BookmarkSearch,
BookmarkSearchForm,
User,
)
register = template.Library()
@@ -30,16 +27,3 @@ def bookmark_search(context, search: BookmarkSearch, mode: str = ""):
"preferences_form": preferences_form,
"mode": mode,
}
@register.inclusion_tag(
"bookmarks/user_select.html", name="user_select", takes_context=True
)
def user_select(context, search: BookmarkSearch, users: List[User]):
sorted_users = sorted(users, key=lambda x: str.lower(x.username))
form = BookmarkSearchForm(search, editable_fields=["user"], users=sorted_users)
return {
"search": search,
"users": sorted_users,
"form": form,
}

View File

@@ -1,29 +1,27 @@
from django.db.models import QuerySet
from django.template import Template, RequestContext
from django.test import TestCase, RequestFactory
from bookmarks.models import BookmarkSearch, User
from bookmarks.tests.helpers import BookmarkFactoryMixin
from bookmarks.views import contexts
class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
def render_template(self, url: str, users: QuerySet[User] = User.objects.all()):
def render_template(self, url: str):
rf = RequestFactory()
request = rf.get(url)
request.user = self.get_or_create_test_user()
request.user_profile = self.get_or_create_test_user().profile
search = BookmarkSearch.from_request(request, request.GET)
user_list = contexts.UserListContext(request, search)
context = RequestContext(
request,
{
"request": request,
"search": search,
"users": users,
"user_list": user_list,
},
)
template_to_render = Template(
"{% load bookmarks %}" "{% user_select search users %}"
)
template_to_render = Template("{% include 'bookmarks/user_section.html' %}")
return template_to_render.render(context)
def assertUserOption(self, html: str, user: User, selected: bool = False):
@@ -49,6 +47,9 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
self.assertNotIn(needle, html)
def test_empty_option(self):
user1 = self.setup_user(name="user1", enable_sharing=True)
self.setup_bookmark(user=user1, shared=True)
rendered_template = self.render_template("/test")
self.assertInHTML(
@@ -59,22 +60,30 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
)
def test_render_user_options(self):
user1 = User.objects.create_user("user1", "user1@example.com", "password123")
user2 = User.objects.create_user("user2", "user2@example.com", "password123")
user3 = User.objects.create_user("user3", "user3@example.com", "password123")
user1 = self.setup_user(name="user1", enable_sharing=True)
user2 = self.setup_user(name="user2", enable_sharing=True)
user3 = self.setup_user(name="user3", enable_sharing=True)
rendered_template = self.render_template("/test", User.objects.all())
self.setup_bookmark(user=user1, shared=True)
self.setup_bookmark(user=user2, shared=True)
self.setup_bookmark(user=user3, shared=True)
rendered_template = self.render_template("/test")
self.assertUserOption(rendered_template, user1)
self.assertUserOption(rendered_template, user2)
self.assertUserOption(rendered_template, user3)
def test_preselect_user_option(self):
user1 = User.objects.create_user("user1", "user1@example.com", "password123")
User.objects.create_user("user2", "user2@example.com", "password123")
User.objects.create_user("user3", "user3@example.com", "password123")
user1 = self.setup_user(name="user1", enable_sharing=True)
user2 = self.setup_user(name="user2", enable_sharing=True)
user3 = self.setup_user(name="user3", enable_sharing=True)
rendered_template = self.render_template("/test?user=user1", User.objects.all())
self.setup_bookmark(user=user1, shared=True)
self.setup_bookmark(user=user2, shared=True)
self.setup_bookmark(user=user3, shared=True)
rendered_template = self.render_template("/test?user=user1")
self.assertUserOption(rendered_template, user1, True)

View File

@@ -55,7 +55,6 @@ def index(request: HttpRequest):
return render_bookmarks_view(
request,
"bookmarks/index.html",
{
"page_title": "Bookmarks - Linkding",
"bookmark_list": bookmark_list,
@@ -95,7 +94,6 @@ def archived(request: HttpRequest):
return render_bookmarks_view(
request,
"bookmarks/archive.html",
{
"page_title": "Archived bookmarks - Linkding",
"bookmark_list": bookmark_list,
@@ -130,19 +128,15 @@ def shared(request: HttpRequest):
bookmark_details = contexts.get_details_context(
request, contexts.SharedBookmarkDetailsContext
)
public_only = not request.user.is_authenticated
users = queries.query_shared_bookmark_users(
request.user_profile, bookmark_list.search, public_only
)
user_list = contexts.UserListContext(request, search)
return render_bookmarks_view(
request,
"bookmarks/shared.html",
{
"page_title": "Shared bookmarks - Linkding",
"bookmark_list": bookmark_list,
"tag_cloud": tag_cloud,
"details": bookmark_details,
"users": users,
"user_list": user_list,
"rss_feed_url": reverse("linkding:feeds.public_shared"),
},
)
@@ -160,7 +154,7 @@ def shared_update(request: HttpRequest):
return render_bookmarks_update(request, bookmark_list, tag_cloud, details)
def render_bookmarks_view(request: HttpRequest, template_name, context):
def render_bookmarks_view(request: HttpRequest, context):
if context["details"]:
context["page_title"] = "Bookmark details - Linkding"
@@ -169,7 +163,7 @@ def render_bookmarks_view(request: HttpRequest, template_name, context):
return render(
request,
template_name,
"bookmarks/bookmark_page.html",
context,
)

View File

@@ -15,6 +15,7 @@ from bookmarks.models import (
BookmarkAsset,
BookmarkBundle,
BookmarkSearch,
BookmarkSearchForm,
User,
UserProfile,
Tag,
@@ -266,14 +267,26 @@ class BookmarkListContext:
class ActiveBookmarkListContext(BookmarkListContext):
list_title = "Bookmarks"
search_mode = ""
bulk_edit_enabled = True
bulk_edit_disabled_actions = "bulk_unarchive"
request_context = ActiveBookmarksContext
class ArchivedBookmarkListContext(BookmarkListContext):
list_title = "Archived bookmarks"
search_mode = "archived"
bulk_edit_enabled = True
bulk_edit_disabled_actions = "bulk_archive"
request_context = ArchivedBookmarksContext
class SharedBookmarkListContext(BookmarkListContext):
list_title = "Shared bookmarks"
search_mode = "shared"
bulk_edit_enabled = False
bulk_edit_disabled_actions = ""
request_context = SharedBookmarksContext
@@ -649,3 +662,13 @@ class BundlesContext:
(bundle for bundle in self.bundles if bundle.id == selected_bundle_id),
None,
)
class UserListContext:
def __init__(self, request: HttpRequest, search: BookmarkSearch) -> None:
public_only = not request.user.is_authenticated
users = queries.query_shared_bookmark_users(
request.user_profile, search, public_only
)
users = sorted(users, key=lambda x: str.lower(x.username))
self.form = BookmarkSearchForm(search, editable_fields=["user"], users=users)