Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bookmarks/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ class TagViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
):
request: HttpRequest
serializer_class = TagSerializer
Expand Down
2 changes: 2 additions & 0 deletions bookmarks/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ class BookmarkSearchForm(forms.Form):
SORT_CHOICES = [
(BookmarkSearch.SORT_ADDED_ASC, "Added ↑"),
(BookmarkSearch.SORT_ADDED_DESC, "Added ↓"),
(BookmarkSearch.SORT_MODIFIED_ASC, "Modified ↑"),
(BookmarkSearch.SORT_MODIFIED_DESC, "Modified ↓"),
(BookmarkSearch.SORT_TITLE_ASC, "Title ↑"),
(BookmarkSearch.SORT_TITLE_DESC, "Title ↓"),
]
Expand Down
2 changes: 2 additions & 0 deletions bookmarks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ def __str__(self):
class BookmarkSearch:
SORT_ADDED_ASC = "added_asc"
SORT_ADDED_DESC = "added_desc"
SORT_MODIFIED_ASC = "modified_asc"
SORT_MODIFIED_DESC = "modified_desc"
SORT_TITLE_ASC = "title_asc"
SORT_TITLE_DESC = "title_desc"

Expand Down
4 changes: 4 additions & 0 deletions bookmarks/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ def _base_bookmarks_query(
query_set = query_set.order_by(order_field).reverse()
elif search.sort == BookmarkSearch.SORT_ADDED_ASC:
query_set = query_set.order_by("date_added")
elif search.sort == BookmarkSearch.SORT_MODIFIED_ASC:
query_set = query_set.order_by("date_modified")
elif search.sort == BookmarkSearch.SORT_MODIFIED_DESC:
query_set = query_set.order_by("-date_modified")
else:
# Sort by date added, descending by default
query_set = query_set.order_by("-date_added")
Expand Down
64 changes: 64 additions & 0 deletions bookmarks/tests/test_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,70 @@ def test_sorty_by_date_added_desc(self):
query = queries.query_bookmarks(self.user, self.profile, search)
self.assertEqual(list(query), sorted_bookmarks)

def test_sort_by_date_modified_asc(self):
search = BookmarkSearch(sort=BookmarkSearch.SORT_MODIFIED_ASC)

bookmarks = [
self.setup_bookmark(
modified=timezone.datetime(2022, 5, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2020, 7, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2023, 4, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2021, 2, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2022, 3, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2020, 1, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2021, 6, 1, tzinfo=datetime.UTC)
),
]
sorted_bookmarks = sorted(bookmarks, key=lambda b: b.date_modified)

query = queries.query_bookmarks(self.user, self.profile, search)
self.assertEqual(list(query), sorted_bookmarks)

def test_sort_by_date_modified_desc(self):
search = BookmarkSearch(sort=BookmarkSearch.SORT_MODIFIED_DESC)

bookmarks = [
self.setup_bookmark(
modified=timezone.datetime(2021, 6, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2023, 4, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2020, 1, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2022, 3, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2021, 2, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2020, 7, 1, tzinfo=datetime.UTC)
),
self.setup_bookmark(
modified=timezone.datetime(2022, 5, 1, tzinfo=datetime.UTC)
),
]
sorted_bookmarks = sorted(
bookmarks, key=lambda b: b.date_modified, reverse=True
)

query = queries.query_bookmarks(self.user, self.profile, search)
self.assertEqual(list(query), sorted_bookmarks)

def setup_title_sort_data(self):
# lots of combinations to test effective title logic
bookmarks = [
Expand Down
41 changes: 39 additions & 2 deletions bookmarks/tests/test_root_view.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from urllib.parse import urlencode

from django.test import TestCase
from django.urls import reverse

Expand All @@ -6,9 +8,17 @@


class RootViewTestCase(TestCase, BookmarkFactoryMixin):
def assertRedirectsToLogin(self, response, next_url):
# The root view redirects to the bookmarks page, which in turn
# redirects unauthenticated users to the login page
self.assertEqual(response.redirect_chain[0], (next_url, 302))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.request["PATH_INFO"], "/login/")
self.assertEqual(response.context["next"], next_url)

def test_unauthenticated_user_redirect_to_login_by_default(self):
response = self.client.get(reverse("linkding:root"))
self.assertRedirects(response, reverse("login"))
response = self.client.get(reverse("linkding:root"), follow=True)
self.assertRedirectsToLogin(response, reverse("linkding:bookmarks.index"))

def test_unauthenticated_redirect_to_shared_bookmarks_if_configured_in_global_settings(
self,
Expand Down Expand Up @@ -38,3 +48,30 @@ def test_authenticated_user_always_redirected_to_bookmarks(self):

response = self.client.get(reverse("linkding:root"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))

def test_authenticated_user_redirect_retains_query_string(self):
self.client.force_login(self.get_or_create_test_user())

query = urlencode({"q": "#china"})
response = self.client.get(reverse("linkding:root") + "?" + query)
self.assertRedirects(
response, reverse("linkding:bookmarks.index") + "?" + query
)

def test_unauthenticated_shared_landing_redirect_retains_query_string(self):
settings = GlobalSettings.get()
settings.landing_page = GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS
settings.save()

query = urlencode({"q": "#china"})
response = self.client.get(reverse("linkding:root") + "?" + query)
self.assertRedirects(
response, reverse("linkding:bookmarks.shared") + "?" + query
)

def test_unauthenticated_login_redirect_retains_query_string(self):
query = urlencode({"q": "#china"})
response = self.client.get(reverse("linkding:root") + "?" + query, follow=True)
self.assertRedirectsToLogin(
response, reverse("linkding:bookmarks.index") + "?" + query
)
41 changes: 41 additions & 0 deletions bookmarks/tests/test_tags_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.urls import reverse
from rest_framework import status

from bookmarks.models import Bookmark, Tag
from bookmarks.tests.helpers import BookmarkFactoryMixin, LinkdingApiTestCase


class TagsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def test_delete_tag(self):
self.authenticate()

tag = self.setup_tag()

url = reverse("linkding:tag-detail", kwargs={"pk": tag.id})
self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)

self.assertFalse(Tag.objects.filter(id=tag.id).exists())

def test_delete_tag_keeps_tagged_bookmarks(self):
self.authenticate()

tag = self.setup_tag()
bookmark = self.setup_bookmark(tags=[tag])

url = reverse("linkding:tag-detail", kwargs={"pk": tag.id})
self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)

self.assertFalse(Tag.objects.filter(id=tag.id).exists())
self.assertTrue(Bookmark.objects.filter(id=bookmark.id).exists())
self.assertEqual(bookmark.tags.count(), 0)

def test_can_not_delete_tag_of_other_user(self):
self.authenticate()

other_user = self.setup_user()
tag = self.setup_tag(user=other_user)

url = reverse("linkding:tag-detail", kwargs={"pk": tag.id})
self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)

self.assertTrue(Tag.objects.filter(id=tag.id).exists())
14 changes: 7 additions & 7 deletions bookmarks/views/root.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
from django.http import HttpResponseRedirect
from django.urls import reverse

from bookmarks.models import GlobalSettings
from bookmarks.utils import redirect_with_query


def root(request):
# Redirect unauthenticated users to the configured landing page
# Redirect unauthenticated users to the shared bookmarks page if that is
# the configured landing page
if not request.user.is_authenticated:
settings = request.global_settings

if settings.landing_page == GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS:
return HttpResponseRedirect(reverse("linkding:bookmarks.shared"))
else:
return HttpResponseRedirect(reverse("login"))
# Retain the query string
return redirect_with_query(request, reverse("linkding:bookmarks.shared"))

# Redirect authenticated users to the bookmarks page
return HttpResponseRedirect(reverse("linkding:bookmarks.index"))
# Otherwise redirect to the bookmarks page, retaining the query string.
return redirect_with_query(request, reverse("linkding:bookmarks.index"))
8 changes: 8 additions & 0 deletions docs/src/content/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,14 @@ Example payload:
}
```

**Delete**

```
DELETE /api/tags/<id>/
```

Deletes a tag by ID.

### Bundles

**List**
Expand Down
Loading