mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-14 05:59:29 +02:00
Create bundle from current search query (#1154)
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.section-header {
|
||||
.section-header:not(.no-wrap) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,29 @@
|
||||
{% if not request.user_profile.hide_bundles %}
|
||||
<section aria-labelledby="bundles-heading">
|
||||
<div class="section-header">
|
||||
<div class="section-header no-wrap">
|
||||
<h2 id="bundles-heading">Bundles</h2>
|
||||
<a href="{% url 'linkding:bundles.index' %}" class="btn ml-auto" aria-label="Manage bundles">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/>
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div ld-dropdown class="dropdown dropdown-right ml-auto">
|
||||
<button class="btn dropdown-toggle" aria-label="Bundles menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="M4 6l16 0"/>
|
||||
<path d="M4 12l16 0"/>
|
||||
<path d="M4 18l16 0"/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="menu" role="list" tabindex="-1">
|
||||
<li class="menu-item">
|
||||
<a href="{% url 'linkding:bundles.index' %}" class="menu-link">Manage bundles</a>
|
||||
</li>
|
||||
{% if bookmark_list.search.q %}
|
||||
<li class="menu-item">
|
||||
<a href="{% url 'linkding:bundles.new' %}?q={{ bookmark_list.search.q|urlencode }}" class="menu-link">Create
|
||||
bundle from search</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="bundle-menu">
|
||||
{% for bundle in bundles.bundles %}
|
||||
|
@@ -120,3 +120,41 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
reverse("linkding:bundles.edit", args=[other_users_bundle.id]), updated_data
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_should_show_correct_preview(self):
|
||||
bundle_tag = self.setup_tag()
|
||||
bookmark1 = self.setup_bookmark(tags=[bundle_tag])
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
bundle = self.setup_bundle(name="Test Bundle", all_tags=bundle_tag.name)
|
||||
|
||||
response = self.client.get(reverse("linkding:bundles.edit", args=[bundle.id]))
|
||||
self.assertContains(response, "Found 1 bookmarks matching this bundle")
|
||||
self.assertContains(response, bookmark1.title)
|
||||
self.assertNotContains(response, bookmark2.title)
|
||||
self.assertNotContains(response, bookmark3.title)
|
||||
|
||||
def test_should_show_correct_preview_after_posting_invalid_data(self):
|
||||
initial_tag = self.setup_tag(name="initial-tag")
|
||||
updated_tag = self.setup_tag(name="updated-tag")
|
||||
bookmark1 = self.setup_bookmark(tags=[initial_tag])
|
||||
bookmark2 = self.setup_bookmark(tags=[updated_tag])
|
||||
bookmark3 = self.setup_bookmark()
|
||||
bundle = self.setup_bundle(name="Test Bundle", all_tags=initial_tag.name)
|
||||
|
||||
form_data = {
|
||||
"name": "",
|
||||
"search": "",
|
||||
"any_tags": "",
|
||||
"all_tags": updated_tag.name,
|
||||
"excluded_tags": "",
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse("linkding:bundles.edit", args=[bundle.id]), form_data
|
||||
)
|
||||
self.assertIn(
|
||||
"Found 1 bookmarks matching this bundle", response.content.decode()
|
||||
)
|
||||
self.assertNotIn(bookmark1.title, response.content.decode())
|
||||
self.assertIn(bookmark2.title, response.content.decode())
|
||||
self.assertNotIn(bookmark3.title, response.content.decode())
|
||||
|
@@ -1,11 +1,12 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from bookmarks.models import BookmarkBundle
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
|
||||
class BundleNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
class BundleNewViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
@@ -75,3 +76,72 @@ class BundleNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
form_data = self.create_form_data({"name": ""})
|
||||
response = self.client.post(reverse("linkding:bundles.new"), form_data)
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
def test_should_prefill_form_from_search_query_parameters(self):
|
||||
query = "machine learning #python #ai"
|
||||
url = reverse("linkding:bundles.new") + "?" + urlencode({"q": query})
|
||||
response = self.client.get(url)
|
||||
|
||||
soup = self.make_soup(response.content.decode())
|
||||
search_field = soup.select_one('input[name="search"]')
|
||||
all_tags_field = soup.select_one('input[name="all_tags"]')
|
||||
|
||||
self.assertEqual(search_field.get("value"), "machine learning")
|
||||
self.assertEqual(all_tags_field.get("value"), "python ai")
|
||||
|
||||
def test_should_ignore_special_search_commands(self):
|
||||
query = "python tutorial !untagged !unread"
|
||||
url = reverse("linkding:bundles.new") + "?" + urlencode({"q": query})
|
||||
response = self.client.get(url)
|
||||
|
||||
soup = self.make_soup(response.content.decode())
|
||||
search_field = soup.select_one('input[name="search"]')
|
||||
all_tags_field = soup.select_one('input[name="all_tags"]')
|
||||
|
||||
self.assertEqual(search_field.get("value"), "python tutorial")
|
||||
self.assertIsNone(all_tags_field.get("value"))
|
||||
|
||||
def test_should_not_prefill_when_no_query_parameter(self):
|
||||
response = self.client.get(reverse("linkding:bundles.new"))
|
||||
|
||||
soup = self.make_soup(response.content.decode())
|
||||
search_field = soup.select_one('input[name="search"]')
|
||||
all_tags_field = soup.select_one('input[name="all_tags"]')
|
||||
|
||||
self.assertIsNone(search_field.get("value"))
|
||||
self.assertIsNone(all_tags_field.get("value"))
|
||||
|
||||
def test_should_not_prefill_when_editing_existing_bundle(self):
|
||||
bundle = self.setup_bundle(
|
||||
name="Existing Bundle", search="Tutorial", all_tags="java spring"
|
||||
)
|
||||
|
||||
query = "machine learning #python #ai"
|
||||
url = (
|
||||
reverse("linkding:bundles.edit", args=[bundle.id])
|
||||
+ "?"
|
||||
+ urlencode({"q": query})
|
||||
)
|
||||
response = self.client.get(url)
|
||||
|
||||
soup = self.make_soup(response.content.decode())
|
||||
search_field = soup.select_one('input[name="search"]')
|
||||
all_tags_field = soup.select_one('input[name="all_tags"]')
|
||||
|
||||
self.assertEqual(search_field.get("value"), "Tutorial")
|
||||
self.assertEqual(all_tags_field.get("value"), "java spring")
|
||||
|
||||
def test_should_show_correct_preview_with_prefilled_values(self):
|
||||
bundle_tag = self.setup_tag()
|
||||
bookmark1 = self.setup_bookmark(tags=[bundle_tag])
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
query = "#" + bundle_tag.name
|
||||
url = reverse("linkding:bundles.new") + "?" + urlencode({"q": query})
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertContains(response, "Found 1 bookmarks matching this bundle")
|
||||
self.assertContains(response, bookmark1.title)
|
||||
self.assertNotContains(response, bookmark2.title)
|
||||
self.assertNotContains(response, bookmark3.title)
|
||||
|
@@ -5,6 +5,7 @@ from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import BookmarkBundle, BookmarkBundleForm, BookmarkSearch
|
||||
from bookmarks.queries import parse_query_string
|
||||
from bookmarks.services import bundles
|
||||
from bookmarks.views import access
|
||||
from bookmarks.views.contexts import ActiveBookmarkListContext
|
||||
@@ -37,7 +38,18 @@ def action(request: HttpRequest):
|
||||
|
||||
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)
|
||||
initial_data = {}
|
||||
if bundle is None and request.method == "GET":
|
||||
query_param = request.GET.get("q")
|
||||
if query_param:
|
||||
parsed = parse_query_string(query_param)
|
||||
|
||||
if parsed["search_terms"]:
|
||||
initial_data["search"] = " ".join(parsed["search_terms"])
|
||||
if parsed["tag_names"]:
|
||||
initial_data["all_tags"] = " ".join(parsed["tag_names"])
|
||||
|
||||
form = BookmarkBundleForm(form_data, instance=bundle, initial=initial_data)
|
||||
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
@@ -53,8 +65,12 @@ def _handle_edit(request: HttpRequest, template: str, bundle: BookmarkBundle = N
|
||||
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}
|
||||
bookmark_list = _get_bookmark_list_preview(request, bundle, initial_data)
|
||||
context = {
|
||||
"form": form,
|
||||
"bundle": bundle,
|
||||
"bookmark_list": bookmark_list,
|
||||
}
|
||||
|
||||
return render(request, template, context, status=status)
|
||||
|
||||
@@ -79,7 +95,9 @@ def preview(request: HttpRequest):
|
||||
|
||||
|
||||
def _get_bookmark_list_preview(
|
||||
request: HttpRequest, bundle: BookmarkBundle | None = None
|
||||
request: HttpRequest,
|
||||
bundle: BookmarkBundle | None = None,
|
||||
initial_data: dict = None,
|
||||
):
|
||||
if request.method == "GET" and bundle:
|
||||
preview_bundle = bundle
|
||||
@@ -87,6 +105,10 @@ def _get_bookmark_list_preview(
|
||||
form_data = (
|
||||
request.POST.copy() if request.method == "POST" else request.GET.copy()
|
||||
)
|
||||
if initial_data:
|
||||
for key, value in initial_data.items():
|
||||
form_data[key] = value
|
||||
|
||||
form_data["name"] = "Preview Bundle" # Set dummy name for form validation
|
||||
form = BookmarkBundleForm(form_data)
|
||||
preview_bundle = form.save(commit=False)
|
||||
|
Reference in New Issue
Block a user