mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-03-01 15:33:13 +08:00
Compare commits
1 Commits
master
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a37967f4a |
3
Makefile
3
Makefile
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -44,8 +44,6 @@ class BookmarkBundleSerializer(serializers.ModelSerializer):
|
||||
"any_tags",
|
||||
"all_tags",
|
||||
"excluded_tags",
|
||||
"filter_unread",
|
||||
"filter_shared",
|
||||
"order",
|
||||
"date_created",
|
||||
"date_modified",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
6
docs/package-lock.json
generated
6
docs/package-lock.json
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user