mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-14 14:09:26 +02:00
Add bundles for organizing bookmarks (#1097)
* add bundle model and query logic * cleanup tests * add basic form * add success message * Add form tests * Add bundle list view * fix edit view * Add remove button * Add basic preview logic * Make pagination use absolute URLs * Hide bookmark edits when rendering preview * Render bookmark list in preview * Reorder bundles * Show bundles in bookmark view * Make bookmark search respect selected bundle * UI tweaks * Fix bookmark scope * Improve bundle preview * Skip preview if form is submitted * Show correct preview after invalid form submission * Add option to hide bundles * Merge new migrations * Add tests for bundle menu * Improve check for preview being removed
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from .assets import *
|
||||
from .auth import *
|
||||
from .bookmarks import *
|
||||
from . import bundles
|
||||
from .settings import *
|
||||
from .toasts import *
|
||||
from .health import health
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from django.http import Http404
|
||||
|
||||
from bookmarks.models import Bookmark, BookmarkAsset, Toast
|
||||
from bookmarks.models import Bookmark, BookmarkAsset, BookmarkBundle, Toast
|
||||
from bookmarks.type_defs import HttpRequest
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@ def bookmark_write(request: HttpRequest, bookmark_id: int | str):
|
||||
raise Http404("Bookmark does not exist")
|
||||
|
||||
|
||||
def bundle_write(request: HttpRequest, bundle_id: int | str):
|
||||
try:
|
||||
return BookmarkBundle.objects.get(pk=bundle_id, owner=request.user)
|
||||
except BookmarkBundle.DoesNotExist:
|
||||
raise Http404("Bundle does not exist")
|
||||
|
||||
|
||||
def asset_read(request: HttpRequest, asset_id: int | str):
|
||||
try:
|
||||
asset = BookmarkAsset.objects.get(pk=asset_id)
|
||||
|
@@ -42,8 +42,12 @@ def index(request: HttpRequest):
|
||||
if request.method == "POST":
|
||||
return search_action(request)
|
||||
|
||||
bookmark_list = contexts.ActiveBookmarkListContext(request)
|
||||
tag_cloud = contexts.ActiveTagCloudContext(request)
|
||||
search = BookmarkSearch.from_request(
|
||||
request, request.GET, request.user_profile.search_preferences
|
||||
)
|
||||
bookmark_list = contexts.ActiveBookmarkListContext(request, search)
|
||||
bundles = contexts.BundlesContext(request)
|
||||
tag_cloud = contexts.ActiveTagCloudContext(request, search)
|
||||
bookmark_details = contexts.get_details_context(
|
||||
request, contexts.ActiveBookmarkDetailsContext
|
||||
)
|
||||
@@ -54,6 +58,7 @@ def index(request: HttpRequest):
|
||||
{
|
||||
"page_title": "Bookmarks - Linkding",
|
||||
"bookmark_list": bookmark_list,
|
||||
"bundles": bundles,
|
||||
"tag_cloud": tag_cloud,
|
||||
"details": bookmark_details,
|
||||
},
|
||||
@@ -65,8 +70,12 @@ def archived(request: HttpRequest):
|
||||
if request.method == "POST":
|
||||
return search_action(request)
|
||||
|
||||
bookmark_list = contexts.ArchivedBookmarkListContext(request)
|
||||
tag_cloud = contexts.ArchivedTagCloudContext(request)
|
||||
search = BookmarkSearch.from_request(
|
||||
request, request.GET, request.user_profile.search_preferences
|
||||
)
|
||||
bookmark_list = contexts.ArchivedBookmarkListContext(request, search)
|
||||
bundles = contexts.BundlesContext(request)
|
||||
tag_cloud = contexts.ArchivedTagCloudContext(request, search)
|
||||
bookmark_details = contexts.get_details_context(
|
||||
request, contexts.ArchivedBookmarkDetailsContext
|
||||
)
|
||||
@@ -77,6 +86,7 @@ def archived(request: HttpRequest):
|
||||
{
|
||||
"page_title": "Archived bookmarks - Linkding",
|
||||
"bookmark_list": bookmark_list,
|
||||
"bundles": bundles,
|
||||
"tag_cloud": tag_cloud,
|
||||
"details": bookmark_details,
|
||||
},
|
||||
@@ -87,8 +97,11 @@ def shared(request: HttpRequest):
|
||||
if request.method == "POST":
|
||||
return search_action(request)
|
||||
|
||||
bookmark_list = contexts.SharedBookmarkListContext(request)
|
||||
tag_cloud = contexts.SharedTagCloudContext(request)
|
||||
search = BookmarkSearch.from_request(
|
||||
request, request.GET, request.user_profile.search_preferences
|
||||
)
|
||||
bookmark_list = contexts.SharedBookmarkListContext(request, search)
|
||||
tag_cloud = contexts.SharedTagCloudContext(request, search)
|
||||
bookmark_details = contexts.get_details_context(
|
||||
request, contexts.SharedBookmarkDetailsContext
|
||||
)
|
||||
@@ -132,13 +145,13 @@ def search_action(request: HttpRequest):
|
||||
if "save" in request.POST:
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseForbidden()
|
||||
search = BookmarkSearch.from_request(request.POST)
|
||||
search = BookmarkSearch.from_request(request, request.POST)
|
||||
request.user_profile.search_preferences = search.preferences_dict
|
||||
request.user_profile.save()
|
||||
|
||||
# redirect to base url including new query params
|
||||
search = BookmarkSearch.from_request(
|
||||
request.POST, request.user_profile.search_preferences
|
||||
request, request.POST, request.user_profile.search_preferences
|
||||
)
|
||||
base_url = request.path
|
||||
query_params = search.query_params
|
||||
@@ -248,7 +261,9 @@ def update_state(request: HttpRequest, bookmark_id: int | str):
|
||||
|
||||
@login_required
|
||||
def index_action(request: HttpRequest):
|
||||
search = BookmarkSearch.from_request(request.GET)
|
||||
search = BookmarkSearch.from_request(
|
||||
request, request.GET, request.user_profile.search_preferences
|
||||
)
|
||||
query = queries.query_bookmarks(request.user, request.user_profile, search)
|
||||
|
||||
response = handle_action(request, query)
|
||||
@@ -263,7 +278,9 @@ def index_action(request: HttpRequest):
|
||||
|
||||
@login_required
|
||||
def archived_action(request: HttpRequest):
|
||||
search = BookmarkSearch.from_request(request.GET)
|
||||
search = BookmarkSearch.from_request(
|
||||
request, request.GET, request.user_profile.search_preferences
|
||||
)
|
||||
query = queries.query_archived_bookmarks(request.user, request.user_profile, search)
|
||||
|
||||
response = handle_action(request, query)
|
||||
|
109
bookmarks/views/bundles.py
Normal file
109
bookmarks/views/bundles.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Max
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import BookmarkBundle, BookmarkBundleForm, BookmarkSearch
|
||||
from bookmarks.views import access
|
||||
from bookmarks.views.contexts import ActiveBookmarkListContext
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request: HttpRequest):
|
||||
bundles = BookmarkBundle.objects.filter(owner=request.user).order_by("order")
|
||||
context = {"bundles": bundles}
|
||||
return render(request, "bundles/index.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def action(request: HttpRequest):
|
||||
if "remove_bundle" in request.POST:
|
||||
remove_bundle_id = request.POST.get("remove_bundle")
|
||||
bundle = access.bundle_write(request, remove_bundle_id)
|
||||
bundle_name = bundle.name
|
||||
bundle.delete()
|
||||
messages.success(request, f"Bundle '{bundle_name}' removed successfully.")
|
||||
|
||||
elif "move_bundle" in request.POST:
|
||||
bundle_id = request.POST.get("move_bundle")
|
||||
move_position = int(request.POST.get("move_position"))
|
||||
bundle_to_move = access.bundle_write(request, bundle_id)
|
||||
user_bundles = list(
|
||||
BookmarkBundle.objects.filter(owner=request.user).order_by("order")
|
||||
)
|
||||
|
||||
if move_position != user_bundles.index(bundle_to_move):
|
||||
user_bundles.remove(bundle_to_move)
|
||||
user_bundles.insert(move_position, bundle_to_move)
|
||||
for bundle_index, bundle in enumerate(user_bundles):
|
||||
bundle.order = bundle_index
|
||||
|
||||
BookmarkBundle.objects.bulk_update(user_bundles, ["order"])
|
||||
|
||||
return HttpResponseRedirect(reverse("linkding:bundles.index"))
|
||||
|
||||
|
||||
def _handle_edit(request: HttpRequest, template: str, bundle: BookmarkBundle = None):
|
||||
form_data = request.POST if request.method == "POST" else None
|
||||
form = BookmarkBundleForm(form_data, instance=bundle)
|
||||
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
instance = form.save(commit=False)
|
||||
instance.owner = request.user
|
||||
|
||||
if bundle is None: # New bundle
|
||||
max_order_result = BookmarkBundle.objects.filter(
|
||||
owner=request.user
|
||||
).aggregate(Max("order", default=-1))
|
||||
instance.order = max_order_result["order__max"] + 1
|
||||
|
||||
instance.save()
|
||||
messages.success(request, "Bundle saved successfully.")
|
||||
return HttpResponseRedirect(reverse("linkding:bundles.index"))
|
||||
|
||||
status = 422 if request.method == "POST" and not form.is_valid() else 200
|
||||
bookmark_list = _get_bookmark_list_preview(request, bundle)
|
||||
context = {"form": form, "bundle": bundle, "bookmark_list": bookmark_list}
|
||||
|
||||
return render(request, template, context, status=status)
|
||||
|
||||
|
||||
@login_required
|
||||
def new(request: HttpRequest):
|
||||
return _handle_edit(request, "bundles/new.html")
|
||||
|
||||
|
||||
@login_required
|
||||
def edit(request: HttpRequest, bundle_id: int):
|
||||
bundle = access.bundle_write(request, bundle_id)
|
||||
|
||||
return _handle_edit(request, "bundles/edit.html", bundle)
|
||||
|
||||
|
||||
@login_required
|
||||
def preview(request: HttpRequest):
|
||||
bookmark_list = _get_bookmark_list_preview(request)
|
||||
context = {"bookmark_list": bookmark_list}
|
||||
return render(request, "bundles/preview.html", context)
|
||||
|
||||
|
||||
def _get_bookmark_list_preview(
|
||||
request: HttpRequest, bundle: BookmarkBundle | None = None
|
||||
):
|
||||
if request.method == "GET" and bundle:
|
||||
preview_bundle = bundle
|
||||
else:
|
||||
form_data = (
|
||||
request.POST.copy() if request.method == "POST" else request.GET.copy()
|
||||
)
|
||||
form_data["name"] = "Preview Bundle" # Set dummy name for form validation
|
||||
form = BookmarkBundleForm(form_data)
|
||||
preview_bundle = form.save(commit=False)
|
||||
|
||||
search = BookmarkSearch(bundle=preview_bundle)
|
||||
bookmark_list = ActiveBookmarkListContext(request, search)
|
||||
bookmark_list.is_preview = True
|
||||
return bookmark_list
|
@@ -13,6 +13,7 @@ from bookmarks import utils
|
||||
from bookmarks.models import (
|
||||
Bookmark,
|
||||
BookmarkAsset,
|
||||
BookmarkBundle,
|
||||
BookmarkSearch,
|
||||
User,
|
||||
UserProfile,
|
||||
@@ -178,15 +179,13 @@ class BookmarkItem:
|
||||
class BookmarkListContext:
|
||||
request_context = RequestContext
|
||||
|
||||
def __init__(self, request: HttpRequest) -> None:
|
||||
def __init__(self, request: HttpRequest, search: BookmarkSearch) -> None:
|
||||
request_context = self.request_context(request)
|
||||
user = request.user
|
||||
user_profile = request.user_profile
|
||||
|
||||
self.request = request
|
||||
self.search = BookmarkSearch.from_request(
|
||||
self.request.GET, user_profile.search_preferences
|
||||
)
|
||||
self.search = search
|
||||
|
||||
query_set = request_context.get_bookmark_query_set(self.search)
|
||||
page_number = request.GET.get("page")
|
||||
@@ -219,6 +218,7 @@ class BookmarkListContext:
|
||||
self.show_preview_images = user_profile.enable_preview_images
|
||||
self.show_notes = user_profile.permanent_notes
|
||||
self.collapse_side_panel = user_profile.collapse_side_panel
|
||||
self.is_preview = False
|
||||
|
||||
@staticmethod
|
||||
def generate_return_url(search: BookmarkSearch, base_url: str, page: int = None):
|
||||
@@ -315,14 +315,12 @@ class TagGroup:
|
||||
class TagCloudContext:
|
||||
request_context = RequestContext
|
||||
|
||||
def __init__(self, request: HttpRequest) -> None:
|
||||
def __init__(self, request: HttpRequest, search: BookmarkSearch) -> None:
|
||||
request_context = self.request_context(request)
|
||||
user_profile = request.user_profile
|
||||
|
||||
self.request = request
|
||||
self.search = BookmarkSearch.from_request(
|
||||
self.request.GET, user_profile.search_preferences
|
||||
)
|
||||
self.search = search
|
||||
|
||||
query_set = request_context.get_tag_query_set(self.search)
|
||||
tags = list(query_set)
|
||||
@@ -461,3 +459,23 @@ def get_details_context(
|
||||
return None
|
||||
|
||||
return context_type(request, bookmark)
|
||||
|
||||
|
||||
class BundlesContext:
|
||||
def __init__(self, request: HttpRequest) -> None:
|
||||
self.request = request
|
||||
self.user = request.user
|
||||
self.user_profile = request.user_profile
|
||||
|
||||
self.bundles = (
|
||||
BookmarkBundle.objects.filter(owner=self.user).order_by("order").all()
|
||||
)
|
||||
self.is_empty = len(self.bundles) == 0
|
||||
|
||||
selected_bundle_id = (
|
||||
int(request.GET.get("bundle")) if request.GET.get("bundle") else None
|
||||
)
|
||||
self.selected_bundle = next(
|
||||
(bundle for bundle in self.bundles if bundle.id == selected_bundle_id),
|
||||
None,
|
||||
)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
from bookmarks.models import BookmarkSearch
|
||||
from bookmarks.views import contexts, turbo
|
||||
|
||||
|
||||
@@ -14,8 +15,11 @@ def render_bookmark_update(request, bookmark_list, tag_cloud, details):
|
||||
|
||||
|
||||
def active_bookmark_update(request):
|
||||
bookmark_list = contexts.ActiveBookmarkListContext(request)
|
||||
tag_cloud = contexts.ActiveTagCloudContext(request)
|
||||
search = BookmarkSearch.from_request(
|
||||
request, request.GET, request.user_profile.search_preferences
|
||||
)
|
||||
bookmark_list = contexts.ActiveBookmarkListContext(request, search)
|
||||
tag_cloud = contexts.ActiveTagCloudContext(request, search)
|
||||
details = contexts.get_details_context(
|
||||
request, contexts.ActiveBookmarkDetailsContext
|
||||
)
|
||||
@@ -23,8 +27,11 @@ def active_bookmark_update(request):
|
||||
|
||||
|
||||
def archived_bookmark_update(request):
|
||||
bookmark_list = contexts.ArchivedBookmarkListContext(request)
|
||||
tag_cloud = contexts.ArchivedTagCloudContext(request)
|
||||
search = BookmarkSearch.from_request(
|
||||
request, request.GET, request.user_profile.search_preferences
|
||||
)
|
||||
bookmark_list = contexts.ArchivedBookmarkListContext(request, search)
|
||||
tag_cloud = contexts.ArchivedTagCloudContext(request, search)
|
||||
details = contexts.get_details_context(
|
||||
request, contexts.ArchivedBookmarkDetailsContext
|
||||
)
|
||||
@@ -32,8 +39,11 @@ def archived_bookmark_update(request):
|
||||
|
||||
|
||||
def shared_bookmark_update(request):
|
||||
bookmark_list = contexts.SharedBookmarkListContext(request)
|
||||
tag_cloud = contexts.SharedTagCloudContext(request)
|
||||
search = BookmarkSearch.from_request(
|
||||
request, request.GET, request.user_profile.search_preferences
|
||||
)
|
||||
bookmark_list = contexts.SharedBookmarkListContext(request, search)
|
||||
tag_cloud = contexts.SharedTagCloudContext(request, search)
|
||||
details = contexts.get_details_context(
|
||||
request, contexts.SharedBookmarkDetailsContext
|
||||
)
|
||||
|
Reference in New Issue
Block a user