Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
4a37967f4a Bump diff from 5.2.0 to 5.2.2 in /docs
Bumps [diff](https://github.com/kpdecker/jsdiff) from 5.2.0 to 5.2.2.
- [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md)
- [Commits](https://github.com/kpdecker/jsdiff/compare/v5.2.0...v5.2.2)

---
updated-dependencies:
- dependency-name: diff
  dependency-version: 5.2.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 18:52:35 +00:00
26 changed files with 66 additions and 475 deletions

View File

@@ -2,7 +2,6 @@
init:
uv sync
[ -d data ] || mkdir data data/assets data/favicons data/previews
uv run manage.py migrate
npm install
@@ -35,4 +34,4 @@ e2e:
uv run pytest bookmarks/tests_e2e -n auto -o "python_files=e2e_test_*.py"
frontend:
npm run dev
npm run dev

View File

@@ -278,8 +278,6 @@ class AdminBookmarkBundle(admin.ModelAdmin):
"any_tags",
"all_tags",
"excluded_tags",
"filter_shared",
"filter_unread",
"date_created",
)
search_fields = ["name", "search", "any_tags", "all_tags", "excluded_tags"]

View File

@@ -44,8 +44,6 @@ class BookmarkBundleSerializer(serializers.ModelSerializer):
"any_tags",
"all_tags",
"excluded_tags",
"filter_unread",
"filter_shared",
"order",
"date_created",
"date_modified",

View File

@@ -218,28 +218,10 @@ class BookmarkBundleForm(forms.ModelForm):
any_tags = forms.CharField(required=False, widget=TagAutocomplete)
all_tags = forms.CharField(required=False, widget=TagAutocomplete)
excluded_tags = forms.CharField(required=False, widget=TagAutocomplete)
filter_unread = forms.ChoiceField(
choices=BookmarkBundle.FILTER_UNREAD_CHOICES,
required=False,
widget=FormSelect,
)
filter_shared = forms.ChoiceField(
choices=BookmarkBundle.FILTER_SHARED_CHOICES,
required=False,
widget=FormSelect,
)
class Meta:
model = BookmarkBundle
fields = [
"name",
"search",
"any_tags",
"all_tags",
"excluded_tags",
"filter_unread",
"filter_shared",
]
fields = ["name", "search", "any_tags", "all_tags", "excluded_tags"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs, error_class=FormErrorList)

View File

@@ -1,30 +0,0 @@
# Generated by Django 6.0 on 2026-02-28 09:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookmarks", "0053_migrate_api_tokens"),
]
operations = [
migrations.AddField(
model_name="bookmarkbundle",
name="filter_shared",
field=models.CharField(
choices=[("off", "All"), ("yes", "Shared"), ("no", "Unshared")],
default="off",
max_length=3,
),
),
migrations.AddField(
model_name="bookmarkbundle",
name="filter_unread",
field=models.CharField(
choices=[("off", "All"), ("yes", "Unread"), ("no", "Read")],
default="off",
max_length=3,
),
),
]

View File

@@ -181,37 +181,11 @@ def bookmark_asset_deleted(sender, instance, **kwargs):
class BookmarkBundle(models.Model):
FILTER_STATE_OFF = "off"
FILTER_STATE_YES = "yes"
FILTER_STATE_NO = "no"
FILTER_UNREAD_CHOICES = [
(FILTER_STATE_OFF, "All"),
(FILTER_STATE_YES, "Unread"),
(FILTER_STATE_NO, "Read"),
]
FILTER_SHARED_CHOICES = [
(FILTER_STATE_OFF, "All"),
(FILTER_STATE_YES, "Shared"),
(FILTER_STATE_NO, "Unshared"),
]
name = models.CharField(max_length=256, blank=False)
search = models.CharField(max_length=256, blank=True)
any_tags = models.CharField(max_length=1024, blank=True)
all_tags = models.CharField(max_length=1024, blank=True)
excluded_tags = models.CharField(max_length=1024, blank=True)
filter_unread = models.CharField(
max_length=3,
choices=FILTER_UNREAD_CHOICES,
blank=False,
default=FILTER_STATE_OFF,
)
filter_shared = models.CharField(
max_length=3,
choices=FILTER_SHARED_CHOICES,
blank=False,
default=FILTER_STATE_OFF,
)
order = models.IntegerField(null=False, default=0)
date_created = models.DateTimeField(auto_now_add=True, null=False)
date_modified = models.DateTimeField(auto_now=True, null=False)

View File

@@ -211,16 +211,6 @@ def _filter_bundle(query_set: QuerySet, bundle: BookmarkBundle) -> QuerySet:
Exists(Bookmark.objects.filter(tag_conditions, id=OuterRef("id")))
)
if bundle.filter_unread == BookmarkBundle.FILTER_STATE_YES:
query_set = query_set.filter(unread=True)
elif bundle.filter_unread == BookmarkBundle.FILTER_STATE_NO:
query_set = query_set.filter(unread=False)
if bundle.filter_shared == BookmarkBundle.FILTER_STATE_YES:
query_set = query_set.filter(shared=True)
elif bundle.filter_shared == BookmarkBundle.FILTER_STATE_NO:
query_set = query_set.filter(shared=False)
return query_set

View File

@@ -157,7 +157,6 @@
{% if bookmark_item.preview_image_file %}
<img class="preview-image"
src="{% static bookmark_item.preview_image_file %}"
alt=""
loading="lazy" />
{% else %}
<div class="preview-image placeholder">

View File

@@ -33,20 +33,6 @@
None of these tags must be present in a bookmark to match.
{% endformhelp %}
</div>
<div class="form-group">
{% formlabel form.filter_unread "Reading State" %}
{% formfield form.filter_unread has_help=True %}
{% formhelp form.filter_unread %}
Limit matches to unread or read bookmarks.
{% endformhelp %}
</div>
<div class="form-group">
{% formlabel form.filter_shared "Sharing State" %}
{% formfield form.filter_shared has_help=True %}
{% formhelp form.filter_shared %}
Limit matches to shared or unshared bookmarks.
{% endformhelp %}
</div>
<div class="form-footer d-flex mt-4">
<input type="submit"
name="save"

View File

@@ -3,8 +3,6 @@
<div>No bookmarks match the current bundle.</div>
{% else %}
<div class="mb-4">Found {{ bookmark_list.bookmarks_total }} bookmarks matching this bundle.</div>
{% with pagination_frame="preview" %}
{% include 'bookmarks/bookmark_list.html' %}
{% endwith %}
{% include 'bookmarks/bookmark_list.html' %}
{% endif %}
</turbo-frame>

View File

@@ -2,9 +2,7 @@
<ul class="pagination">
{% if prev_link %}
<li class="page-item">
<a href="{{ prev_link }}"
tabindex="-1"
data-turbo-frame="{{ pagination_frame }}">Previous</a>
<a href="{{ prev_link }}" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
@@ -14,7 +12,7 @@
{% for page_link in page_links %}
{% if page_link %}
<li class="page-item {% if page_link.active %}active{% endif %}">
<a href="{{ page_link.link }}" data-turbo-frame="{{ pagination_frame }}">{{ page_link.number }}</a>
<a href="{{ page_link.link }}">{{ page_link.number }}</a>
</li>
{% else %}
<li class="page-item">
@@ -24,9 +22,7 @@
{% endfor %}
{% if next_link %}
<li class="page-item">
<a href="{{ next_link }}"
tabindex="-1"
data-turbo-frame="{{ pagination_frame }}">Next</a>
<a href="{{ next_link }}" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">

View File

@@ -1,11 +1,11 @@
<turbo-frame id="tag-modal">
<form method="post"
action="{% url 'linkding:tags.edit' tag.id %}?{{ request.GET.urlencode }}"
data-turbo-frame="tag-main"
action="{% url 'linkding:tags.edit' tag.id %}"
data-turbo-frame="_top"
novalidate>
{% csrf_token %}
<ld-modal class="modal tag-edit-modal active"
data-close-url="{% url 'linkding:tags.index' %}?{{ request.GET.urlencode }}"
data-close-url="{% url 'linkding:tags.index' %}"
data-turbo-frame="tag-modal">
<div class="modal-overlay" data-close-modal></div>
<div class="modal-container" role="dialog" aria-modal="true">

View File

@@ -5,7 +5,6 @@
{% endblock %}
{% block content %}
<div class="tags-page crud-page">
<turbo-frame id="tag-main">
<main aria-labelledby="main-heading">
<div class="crud-header">
<h1 id="main-heading">Tags</h1>
@@ -22,7 +21,7 @@
{# Filters #}
<div class="crud-filters">
<ld-form data-form-reset>
<form method="get" class="mb-2" data-turbo-frame="_top">
<form method="get" class="mb-2">
<div class="form-group">
<label class="form-label text-assistive" for="search">Search tags</label>
<div class="input-group">
@@ -98,7 +97,7 @@
</td>
<td class="actions">
<a class="btn btn-link"
href="{% url 'linkding:tags.edit' tag.id %}?{{ request.GET.urlencode }}"
href="{% url 'linkding:tags.edit' tag.id %}"
data-turbo-frame="tag-modal">Edit</a>
<button type="submit"
name="delete_tag"
@@ -124,7 +123,6 @@
</div>
{% endif %}
</main>
<turbo-frame id="tag-modal"></turbo-frame>
</turbo-frame>
</div>
<turbo-frame id="tag-modal"></turbo-frame>
{% endblock %}

View File

@@ -12,7 +12,6 @@ register = template.Library()
@register.inclusion_tag("shared/pagination.html", name="pagination", takes_context=True)
def pagination(context, page: Page):
request = context["request"]
pagination_frame = context.get("pagination_frame", "_top")
base_url = request.path
# remove page number and details from query parameters
@@ -52,7 +51,6 @@ def pagination(context, page: Page):
"prev_link": prev_link,
"next_link": next_link,
"page_links": page_links,
"pagination_frame": pagination_frame,
}

View File

@@ -179,8 +179,6 @@ class BookmarkFactoryMixin:
any_tags: str = "",
all_tags: str = "",
excluded_tags: str = "",
filter_unread: str = BookmarkBundle.FILTER_STATE_OFF,
filter_shared: str = BookmarkBundle.FILTER_STATE_OFF,
order: int = 0,
):
if user is None:
@@ -195,8 +193,6 @@ class BookmarkFactoryMixin:
any_tags=any_tags,
all_tags=all_tags,
excluded_tags=excluded_tags,
filter_unread=filter_unread,
filter_shared=filter_shared,
order=order,
)
bundle.save()

View File

@@ -13,8 +13,6 @@ class BundlesApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bundle.any_tags, data["any_tags"])
self.assertEqual(bundle.all_tags, data["all_tags"])
self.assertEqual(bundle.excluded_tags, data["excluded_tags"])
self.assertEqual(bundle.filter_unread, data["filter_unread"])
self.assertEqual(bundle.filter_shared, data["filter_shared"])
self.assertEqual(bundle.order, data["order"])
self.assertEqual(
bundle.date_created.isoformat().replace("+00:00", "Z"), data["date_created"]
@@ -73,8 +71,6 @@ class BundlesApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
any_tags="tag1 tag2",
all_tags="required-tag",
excluded_tags="excluded-tag",
filter_unread=BookmarkBundle.FILTER_STATE_YES,
filter_shared=BookmarkBundle.FILTER_STATE_NO,
order=5,
)
@@ -106,8 +102,6 @@ class BundlesApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
"any_tags": "tag1 tag2",
"all_tags": "required-tag",
"excluded_tags": "excluded-tag",
"filter_unread": BookmarkBundle.FILTER_STATE_YES,
"filter_shared": BookmarkBundle.FILTER_STATE_NO,
}
url = reverse("linkding:bundle-list")
@@ -121,8 +115,6 @@ class BundlesApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bundle.any_tags, bundle_data["any_tags"])
self.assertEqual(bundle.all_tags, bundle_data["all_tags"])
self.assertEqual(bundle.excluded_tags, bundle_data["excluded_tags"])
self.assertEqual(bundle.filter_unread, bundle_data["filter_unread"])
self.assertEqual(bundle.filter_shared, bundle_data["filter_shared"])
self.assertEqual(bundle.owner, self.user)
self.assertEqual(bundle.order, 0)
@@ -209,8 +201,6 @@ class BundlesApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
"any_tags": "updated-tag1 updated-tag2",
"all_tags": "required-updated-tag",
"excluded_tags": "excluded-updated-tag",
"filter_unread": BookmarkBundle.FILTER_STATE_YES,
"filter_shared": BookmarkBundle.FILTER_STATE_NO,
"order": 5,
}
@@ -223,8 +213,6 @@ class BundlesApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bundle.any_tags, updated_data["any_tags"])
self.assertEqual(bundle.all_tags, updated_data["all_tags"])
self.assertEqual(bundle.excluded_tags, updated_data["excluded_tags"])
self.assertEqual(bundle.filter_unread, updated_data["filter_unread"])
self.assertEqual(bundle.filter_shared, updated_data["filter_shared"])
self.assertEqual(bundle.order, updated_data["order"])
self.assertBundle(bundle, response.data)

View File

@@ -1,7 +1,6 @@
from django.test import TestCase
from django.urls import reverse
from bookmarks.models import BookmarkBundle
from bookmarks.tests.helpers import BookmarkFactoryMixin
@@ -19,8 +18,6 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
"any_tags": "tag1 tag2",
"all_tags": "required-tag",
"excluded_tags": "excluded-tag",
"filter_unread": BookmarkBundle.FILTER_STATE_YES,
"filter_shared": BookmarkBundle.FILTER_STATE_NO,
}
return {**form_data, **overrides}
@@ -41,8 +38,6 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(bundle.any_tags, updated_data["any_tags"])
self.assertEqual(bundle.all_tags, updated_data["all_tags"])
self.assertEqual(bundle.excluded_tags, updated_data["excluded_tags"])
self.assertEqual(bundle.filter_unread, updated_data["filter_unread"])
self.assertEqual(bundle.filter_shared, updated_data["filter_shared"])
def test_should_render_edit_form_with_prefilled_fields(self):
bundle = self.setup_bundle(
@@ -51,8 +46,6 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
any_tags="tag1 tag2 tag3",
all_tags="required-tag all-tag",
excluded_tags="excluded-tag banned-tag",
filter_unread=BookmarkBundle.FILTER_STATE_YES,
filter_shared=BookmarkBundle.FILTER_STATE_NO,
)
response = self.client.get(reverse("linkding:bundles.edit", args=[bundle.id]))
@@ -102,30 +95,6 @@ class BundleEditViewTestCase(TestCase, BookmarkFactoryMixin):
html,
)
self.assertInHTML(
"""
<select name="filter_unread" class="form-select"
aria-describedby="id_filter_unread_help" id="id_filter_unread">
<option value="off">All</option>
<option value="yes" selected>Unread</option>
<option value="no">Read</option>
</select>
""",
html,
)
self.assertInHTML(
"""
<select name="filter_shared" class="form-select"
aria-describedby="id_filter_shared_help" id="id_filter_shared">
<option value="off">All</option>
<option value="yes">Shared</option>
<option value="no" selected>Unshared</option>
</select>
""",
html,
)
def test_should_return_422_with_invalid_form(self):
bundle = self.setup_bundle(
name="Test Bundle",

View File

@@ -21,8 +21,6 @@ class BundleNewViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
"any_tags": "tag1 tag2",
"all_tags": "required-tag",
"excluded_tags": "excluded-tag",
"filter_unread": BookmarkBundle.FILTER_STATE_YES,
"filter_shared": BookmarkBundle.FILTER_STATE_NO,
}
return {**form_data, **overrides}
@@ -40,8 +38,6 @@ class BundleNewViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertEqual(bundle.any_tags, form_data["any_tags"])
self.assertEqual(bundle.all_tags, form_data["all_tags"])
self.assertEqual(bundle.excluded_tags, form_data["excluded_tags"])
self.assertEqual(bundle.filter_unread, form_data["filter_unread"])
self.assertEqual(bundle.filter_shared, form_data["filter_shared"])
self.assertRedirects(response, reverse("linkding:bundles.index"))

View File

@@ -1,7 +1,6 @@
from django.test import TestCase
from django.urls import reverse
from bookmarks.models import BookmarkBundle
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
@@ -94,68 +93,6 @@ class BundlePreviewViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertContains(response, active_bookmark.title)
self.assertNotContains(response, archived_bookmark.title)
def test_preview_with_filter_unread(self):
unread_bookmark = self.setup_bookmark(title="Unread Bookmark", unread=True)
read_bookmark = self.setup_bookmark(title="Read Bookmark", unread=False)
# Filter unread
response = self.client.get(
reverse("linkding:bundles.preview"),
{"filter_unread": BookmarkBundle.FILTER_STATE_YES},
)
self.assertContains(response, "Found 1 bookmarks matching this bundle")
self.assertContains(response, unread_bookmark.title)
self.assertNotContains(response, read_bookmark.title)
# Filter read
response = self.client.get(
reverse("linkding:bundles.preview"),
{"filter_unread": BookmarkBundle.FILTER_STATE_NO},
)
self.assertContains(response, "Found 1 bookmarks matching this bundle")
self.assertNotContains(response, unread_bookmark.title)
self.assertContains(response, read_bookmark.title)
# Filter off
response = self.client.get(
reverse("linkding:bundles.preview"),
{"filter_unread": BookmarkBundle.FILTER_STATE_OFF},
)
self.assertContains(response, "Found 2 bookmarks matching this bundle")
self.assertContains(response, unread_bookmark.title)
self.assertContains(response, read_bookmark.title)
def test_preview_with_filter_shared(self):
shared_bookmark = self.setup_bookmark(title="Shared Bookmark", shared=True)
unshared_bookmark = self.setup_bookmark(title="Unshared Bookmark", shared=False)
# Filter shared
response = self.client.get(
reverse("linkding:bundles.preview"),
{"filter_shared": BookmarkBundle.FILTER_STATE_YES},
)
self.assertContains(response, "Found 1 bookmarks matching this bundle")
self.assertContains(response, shared_bookmark.title)
self.assertNotContains(response, unshared_bookmark.title)
# Filter unshared
response = self.client.get(
reverse("linkding:bundles.preview"),
{"filter_shared": BookmarkBundle.FILTER_STATE_NO},
)
self.assertContains(response, "Found 1 bookmarks matching this bundle")
self.assertNotContains(response, shared_bookmark.title)
self.assertContains(response, unshared_bookmark.title)
# Filter off
response = self.client.get(
reverse("linkding:bundles.preview"),
{"filter_shared": BookmarkBundle.FILTER_STATE_OFF},
)
self.assertContains(response, "Found 2 bookmarks matching this bundle")
self.assertContains(response, shared_bookmark.title)
self.assertContains(response, unshared_bookmark.title)
def test_preview_requires_authentication(self):
self.client.logout()

View File

@@ -7,12 +7,7 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class PaginationTagTest(TestCase, BookmarkFactoryMixin):
def render_template(
self,
num_items: int,
page_size: int,
current_page: int,
url: str = "/test",
frame: str = None,
self, num_items: int, page_size: int, current_page: int, url: str = "/test"
) -> str:
rf = RequestFactory()
request = rf.get(url)
@@ -21,11 +16,7 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
paginator = Paginator(range(0, num_items), page_size)
page = paginator.page(current_page)
context_dict = {"page": page}
if frame:
context_dict["pagination_frame"] = frame
context = RequestContext(request, context_dict)
context = RequestContext(request, {"page": page})
template_to_render = Template("{% load pagination %}{% pagination page %}")
return template_to_render.render(context)
@@ -39,14 +30,12 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
html,
)
def assertPrevLink(
self, html: str, page_number: int, href: str = None, frame: str = "_top"
):
def assertPrevLink(self, html: str, page_number: int, href: str = None):
href = href if href else f"/test?page={page_number}"
self.assertInHTML(
f"""
<li class="page-item">
<a href="{href}" tabindex="-1" data-turbo-frame="{frame}">Previous</a>
<a href="{href}" tabindex="-1">Previous</a>
</li>
""",
html,
@@ -62,14 +51,12 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
html,
)
def assertNextLink(
self, html: str, page_number: int, href: str = None, frame: str = "_top"
):
def assertNextLink(self, html: str, page_number: int, href: str = None):
href = href if href else f"/test?page={page_number}"
self.assertInHTML(
f"""
<li class="page-item">
<a href="{href}" tabindex="-1" data-turbo-frame="{frame}">Next</a>
<a href="{href}" tabindex="-1">Next</a>
</li>
""",
html,
@@ -82,14 +69,13 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
active: bool,
count: int = 1,
href: str = None,
frame: str = "_top",
):
active_class = "active" if active else ""
href = href if href else f"/test?page={page_number}"
self.assertInHTML(
f"""
<li class="page-item {active_class}">
<a href="{href}" data-turbo-frame="{frame}">{page_number}</a>
<a href="{href}">{page_number}</a>
</li>
""",
html,
@@ -202,10 +188,3 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
self.assertPageLink(rendered_template, 1, False, href="/test?page=1")
self.assertPageLink(rendered_template, 2, True, href="/test?page=2")
self.assertNextLink(rendered_template, 3, href="/test?page=3")
def test_respects_pagination_frame(self):
rendered_template = self.render_template(100, 10, 2, frame="my_frame")
self.assertPrevLink(rendered_template, 1, frame="my_frame")
self.assertPageLink(rendered_template, 1, False, frame="my_frame")
self.assertPageLink(rendered_template, 2, True, frame="my_frame")
self.assertNextLink(rendered_template, 3, frame="my_frame")

View File

@@ -6,7 +6,7 @@ from django.test import TestCase
from django.utils import timezone
from bookmarks import queries
from bookmarks.models import BookmarkBundle, BookmarkSearch, UserProfile
from bookmarks.models import BookmarkSearch, UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, random_sentence
from bookmarks.utils import unique
@@ -1501,89 +1501,6 @@ class QueriesBasicTestCase(TestCase, BookmarkFactoryMixin):
)
self.assertQueryResult(query, [matching_bookmarks])
def test_query_bookmarks_with_bundle_filter_unread(self):
unread_bookmarks = [
self.setup_bookmark(unread=True),
self.setup_bookmark(unread=True),
]
read_bookmarks = [
self.setup_bookmark(unread=False),
self.setup_bookmark(unread=False),
]
# Filter unread
bundle = self.setup_bundle(filter_unread=BookmarkBundle.FILTER_STATE_YES)
query = queries.query_bookmarks(
self.user, self.profile, BookmarkSearch(q="", bundle=bundle)
)
self.assertQueryResult(query, [unread_bookmarks])
# Filter read
bundle = self.setup_bundle(filter_unread=BookmarkBundle.FILTER_STATE_NO)
query = queries.query_bookmarks(
self.user, self.profile, BookmarkSearch(q="", bundle=bundle)
)
self.assertQueryResult(query, [read_bookmarks])
# Filter off
bundle = self.setup_bundle(filter_unread=BookmarkBundle.FILTER_STATE_OFF)
query = queries.query_bookmarks(
self.user, self.profile, BookmarkSearch(q="", bundle=bundle)
)
self.assertQueryResult(query, [unread_bookmarks, read_bookmarks])
def test_query_bookmarks_with_bundle_filter_shared(self):
shared_bookmarks = [
self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True),
]
unshared_bookmarks = [
self.setup_bookmark(shared=False),
self.setup_bookmark(shared=False),
]
# Filter shared
bundle = self.setup_bundle(filter_shared=BookmarkBundle.FILTER_STATE_YES)
query = queries.query_bookmarks(
self.user, self.profile, BookmarkSearch(q="", bundle=bundle)
)
self.assertQueryResult(query, [shared_bookmarks])
# Filter unshared
bundle = self.setup_bundle(filter_shared=BookmarkBundle.FILTER_STATE_NO)
query = queries.query_bookmarks(
self.user, self.profile, BookmarkSearch(q="", bundle=bundle)
)
self.assertQueryResult(query, [unshared_bookmarks])
# Filter off
bundle = self.setup_bundle(filter_shared=BookmarkBundle.FILTER_STATE_OFF)
query = queries.query_bookmarks(
self.user, self.profile, BookmarkSearch(q="", bundle=bundle)
)
self.assertQueryResult(query, [shared_bookmarks, unshared_bookmarks])
def test_query_bookmarks_with_bundle_unread_shared_filters_combined(self):
bundle = self.setup_bundle(
search="python",
filter_unread=BookmarkBundle.FILTER_STATE_YES,
filter_shared=BookmarkBundle.FILTER_STATE_NO,
)
matching_bookmarks = [
self.setup_bookmark(title="Python Tutorial", unread=True, shared=False),
]
# Bookmarks that should not match
self.setup_bookmark(title="Python Guide", unread=False, shared=False)
self.setup_bookmark(title="Python Docs", unread=True, shared=True)
self.setup_bookmark(title="Java Guide", unread=True, shared=False)
query = queries.query_bookmarks(
self.user, self.profile, BookmarkSearch(q="", bundle=bundle)
)
self.assertQueryResult(query, [matching_bookmarks])
def test_query_archived_bookmarks_with_bundle(self):
bundle = self.setup_bundle(any_tags="bundleTag1 bundleTag2")

View File

@@ -90,17 +90,20 @@ class TagsEditViewTestCase(TestCase, BookmarkFactoryMixin):
tag2.refresh_from_db()
self.assertEqual(tag2.name, "tag1")
def test_update_tag_preserves_query_parameters(self):
def test_update_shows_success_message(self):
tag = self.setup_tag(name="old_name")
url = (
reverse("linkding:tags.edit", args=[tag.id])
+ "?search=search&unused=true&page=2&sort=name-desc"
response = self.client.post(
reverse("linkding:tags.edit", args=[tag.id]),
{"name": "new_name"},
follow=True,
)
response = self.client.post(url, {"name": "new_name"})
expected_redirect = (
reverse("linkding:tags.index")
+ "?search=search&unused=true&page=2&sort=name-desc"
self.assertInHTML(
"""
<div class="toast toast-success" role="alert">
Tag "new_name" updated successfully.
</div>
""",
response.content.decode(),
)
self.assertRedirects(response, expected_redirect)

View File

@@ -153,6 +153,24 @@ class TagsIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertRedirects(response, reverse("linkding:tags.index"))
self.assertFalse(Tag.objects.filter(id=tag.id).exists())
def test_tag_delete_action_shows_success_message(self):
tag = self.setup_tag(name="tag_to_delete")
response = self.client.post(
reverse("linkding:tags.index"), {"delete_tag": tag.id}, follow=True
)
self.assertEqual(response.status_code, 200)
self.assertInHTML(
"""
<div class="toast toast-success" role="alert">
Tag "tag_to_delete" deleted successfully.
</div>
""",
response.content.decode(),
)
def test_tag_delete_action_preserves_query_parameters(self):
tag = self.setup_tag(name="search_tag")

View File

@@ -113,6 +113,9 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
# Verify modal is closed
expect(modal).not_to_be_visible()
# Verify the success message is shown
self.verify_success_message('Tag "new-name" updated successfully.')
# Verify the updated tag is shown in the list
expect(self.locate_tag_row("new-name")).to_be_visible()
expect(self.locate_tag_row("old-name")).not_to_be_visible()
@@ -154,89 +157,6 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
tag.refresh_from_db()
self.assertEqual(tag.name, "tag-to-edit")
def test_edit_tag_preserves_query_and_scroll_position(self):
# Create enough tags to have multiple pages (50 per page)
for i in range(70):
self.setup_tag(name=f"test-tag-{i:02d}")
# Open tags page 2 with search query
url = reverse("linkding:tags.index") + "?search=test&page=2"
self.open(url)
# Verify we're on page 2
expect(self.locate_tag_row("test-tag-00")).not_to_be_visible()
expect(self.locate_tag_row("test-tag-50")).to_be_visible()
expect(self.locate_tag_row("test-tag-60")).to_be_visible()
# Scroll down
self.page.evaluate("window.scrollTo(0, 300)")
initial_scroll = self.page.evaluate("window.scrollY")
self.assertGreater(initial_scroll, 0)
# Edit tag
tag_row = self.locate_tag_row("test-tag-55")
tag_row.get_by_role("link", name="Edit").click()
modal = self.locate_tag_modal()
name_input = modal.get_by_label("Name")
name_input.fill("test-tag-55-edited")
modal.get_by_text("Save").click()
expect(modal).not_to_be_visible()
# Verify query parameters and scroll position are preserved
current_url = self.page.url
self.assertIn("search=test", current_url)
self.assertIn("page=2", current_url)
expect(self.locate_tag_row("test-tag-00")).not_to_be_visible()
expect(self.locate_tag_row("test-tag-50")).to_be_visible()
expect(self.locate_tag_row("test-tag-55-edited")).to_be_visible()
expect(self.locate_tag_row("test-tag-60")).to_be_visible()
final_scroll = self.page.evaluate("window.scrollY")
self.assertEqual(initial_scroll, final_scroll)
def test_delete_tag_preserves_query_and_scroll_position(self):
# Create enough tags to have multiple pages (50 per page)
for i in range(70):
self.setup_tag(name=f"test-tag-{i:02d}")
# Open tags page 2 with search query
url = reverse("linkding:tags.index") + "?search=test&page=2"
self.open(url)
# Verify we're on page 2
expect(self.locate_tag_row("test-tag-00")).not_to_be_visible()
expect(self.locate_tag_row("test-tag-50")).to_be_visible()
expect(self.locate_tag_row("test-tag-55")).to_be_visible()
expect(self.locate_tag_row("test-tag-60")).to_be_visible()
# Scroll down
self.page.evaluate("window.scrollTo(0, 300)")
initial_scroll = self.page.evaluate("window.scrollY")
self.assertGreater(initial_scroll, 0)
# Delete tag
tag_row = self.locate_tag_row("test-tag-55")
tag_row.get_by_role("button", name="Remove").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Verify query parameters and scroll position are preserved
current_url = self.page.url
self.assertIn("search=test", current_url)
self.assertIn("page=2", current_url)
expect(self.locate_tag_row("test-tag-00")).not_to_be_visible()
expect(self.locate_tag_row("test-tag-50")).to_be_visible()
expect(self.locate_tag_row("test-tag-55")).not_to_be_visible()
expect(self.locate_tag_row("test-tag-60")).to_be_visible()
final_scroll = self.page.evaluate("window.scrollY")
self.assertEqual(initial_scroll, final_scroll)
def test_merge_tags(self):
target_tag = self.setup_tag(name="target-tag")
merge_tag1 = self.setup_tag(name="merge-tag1")
@@ -334,28 +254,3 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
self.assertEqual(
Tag.objects.filter(owner=self.get_or_create_test_user()).count(), 2
)
def test_search_updates_url_query_params(self):
self.setup_tag(name="python")
self.setup_tag(name="javascript")
self.setup_tag(name="typescript")
self.open(reverse("linkding:tags.index"))
# Verify all tags are visible initially
expect(self.locate_tag_row("python")).to_be_visible()
expect(self.locate_tag_row("javascript")).to_be_visible()
expect(self.locate_tag_row("typescript")).to_be_visible()
# Enter search term and submit
search_input = self.page.get_by_placeholder("Search tags...")
search_input.fill("script")
self.page.get_by_role("button", name="Search").click()
# Wait for filtered results to appear
expect(self.locate_tag_row("python")).not_to_be_visible()
expect(self.locate_tag_row("javascript")).to_be_visible()
expect(self.locate_tag_row("typescript")).to_be_visible()
# Verify URL contains search query param
self.assertIn("search=script", self.page.url)

View File

@@ -10,7 +10,6 @@ from django.urls import reverse
from bookmarks.forms import TagForm, TagMergeForm
from bookmarks.models import Bookmark, Tag
from bookmarks.type_defs import HttpRequest
from bookmarks.utils import redirect_with_query
from bookmarks.views import turbo
@@ -19,8 +18,15 @@ def tags_index(request: HttpRequest):
if request.method == "POST" and "delete_tag" in request.POST:
tag_id = request.POST.get("delete_tag")
tag = get_object_or_404(Tag, id=tag_id, owner=request.user)
tag_name = tag.name
tag.delete()
return redirect_with_query(request, reverse("linkding:tags.index"))
messages.success(request, f'Tag "{tag_name}" deleted successfully.')
redirect_url = reverse("linkding:tags.index")
if request.GET:
redirect_url += "?" + request.GET.urlencode()
return HttpResponseRedirect(redirect_url)
search = request.GET.get("search", "").strip()
unused_only = request.GET.get("unused", "") == "true"
@@ -94,7 +100,8 @@ def tag_edit(request: HttpRequest, tag_id: int):
if request.method == "POST":
if form.is_valid():
form.save()
return redirect_with_query(request, reverse("linkding:tags.index"))
messages.success(request, f'Tag "{tag.name}" updated successfully.')
return HttpResponseRedirect(reverse("linkding:tags.index"))
else:
return turbo.stream(
turbo.replace(

View File

@@ -2566,9 +2566,9 @@
"license": "MIT"
},
"node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz",
"integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"