Compare commits

...

37 Commits

Author SHA1 Message Date
Sascha Ißbrücker
e487cf726a Bump version 2025-05-17 10:53:04 +02:00
Bastian
f2800efc1a Allow pre-filling tags in new bookmark form (#1060)
* feat - Allow tag_string as query for BookmarkForm in order to set tags via bookmark snippets

* add test

---------

Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
2025-05-17 10:13:07 +02:00
Johannes Zorn
9a00ae4b93 Add opensearch declaration (#1058)
* feat: Add opensearch declaration

* cleanup

---------

Co-authored-by: Johannes Zorn <johannes.zorn@zollsoft.com>
Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
2025-05-17 09:52:26 +02:00
dependabot[bot]
da9371e33c Bump django from 5.1.8 to 5.1.9 (#1059)
Bumps [django](https://github.com/django/django) from 5.1.8 to 5.1.9.
- [Commits](https://github.com/django/django/compare/5.1.8...5.1.9)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.1.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-17 09:03:58 +02:00
Jakob Krigovsky
5b3f2f6563 Linkify plain URLs in notes (#1051)
* Linkify plain URLs in notes

* add test case

---------

Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
2025-05-17 09:03:40 +02:00
Kazi
04065f8079 Add how-to for using linkding PWA in native Android share sheet (#1055) 2025-05-17 08:58:29 +02:00
Vincent Ging Ho Yim
d986ff0900 Fix typo in index.mdx tagline (#1052) 2025-05-17 08:56:42 +02:00
dependabot[bot]
51a85bbaf1 Bump esbuild, @astrojs/starlight and astro in /docs (#1037)
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.2 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [@astrojs/starlight](https://github.com/withastro/starlight/tree/HEAD/packages/starlight) and [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro). These dependencies need to be updated together.


Updates `esbuild` from 0.21.5 to 0.25.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.2)

Updates `@astrojs/starlight` from 0.27.1 to 0.32.5
- [Release notes](https://github.com/withastro/starlight/releases)
- [Changelog](https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md)
- [Commits](https://github.com/withastro/starlight/commits/@astrojs/starlight@0.32.5/packages/starlight)

Updates `astro` from 4.16.18 to 5.6.0
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@5.6.0/packages/astro)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.2
  dependency-type: indirect
- dependency-name: "@astrojs/starlight"
  dependency-version: 0.32.5
  dependency-type: direct:production
- dependency-name: astro
  dependency-version: 5.6.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-17 08:48:00 +02:00
dependabot[bot]
39b911880d Bump vite from 5.4.14 to 5.4.17 in /docs (#1036)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.14 to 5.4.17.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.17/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.17/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.17
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-17 08:33:58 +02:00
dependabot[bot]
9db3fa1248 Bump @babel/helpers from 7.26.7 to 7.27.0 in /docs (#1035)
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.26.7 to 7.27.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.27.0/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-version: 7.27.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-17 08:33:44 +02:00
dependabot[bot]
77689366a0 Bump prismjs from 1.29.0 to 1.30.0 in /docs (#1034)
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.29.0 to 1.30.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.29.0...v1.30.0)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-version: 1.30.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-17 08:33:31 +02:00
Cayce House
f2e6014ca4 Push Docker images to GHCR in addition to Docker Hub (#1024) 2025-05-17 08:33:11 +02:00
haondt
da98929f07 Adding linktiles to community projects (#1025)
* added linktiles link

* switched to github link
2025-05-17 08:26:03 +02:00
Sascha Ißbrücker
1b0684bd6c Allow auto tagging rules to match URL fragments (#1045) 2025-04-13 09:43:11 +02:00
dependabot[bot]
8928c78530 Bump tar-fs in /docs (#1028)
Bumps  and [tar-fs](https://github.com/mafintosh/tar-fs). These dependencies needed to be updated together.

Updates `tar-fs` from 2.1.1 to 3.0.8
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.1...v3.0.8)

Updates `tar-fs` from 3.0.6 to 3.0.8
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.1...v3.0.8)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-type: indirect
- dependency-name: tar-fs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 21:17:32 +02:00
dependabot[bot]
61108234b4 Bump django from 5.1.7 to 5.1.8 (#1030)
Bumps [django](https://github.com/django/django) from 5.1.7 to 5.1.8.
- [Commits](https://github.com/django/django/compare/5.1.7...5.1.8)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 21:17:19 +02:00
Sascha Ißbrücker
7b098d4549 Fix bookmark asset download endpoint (#1033) 2025-04-03 21:16:59 +02:00
Sascha Ißbrücker
648e67bd91 Update troubleshooting.md 2025-03-22 22:19:23 +01:00
Sascha Ißbrücker
6bba4f35c8 Prefer local snapshot over web archive link in bookmark list links (#1021)
* Prefer local snapshot over web archive link

* Update latest snapshot when it is deleted

* fix filter in migration

* improve migration performance
2025-03-22 19:07:05 +01:00
Sascha Ißbrücker
6d9d0e19f1 Add E2E tests for refresh button 2025-03-22 12:16:48 +01:00
Josh S
a23c357f2f Add bulk and single bookmark metadata refresh (#999)
* Add url create/edit query paramter to clear cache

* Add refresh bookmark metadata button in create/edit bookmark page

* Fix refresh bookmark metadata when editing existing bookmark

* Add bulk refresh metadata functionality

* Fix test cases for bulk view dropdown selection list

* Allow bulk metadata refresh when background tasks are disabled

* Move load preview image call on refresh metadata

* Update bookmark modified time on metadata refresh

* Rename function to align with convention

* Add tests for refresh task

* Add tests for bookmarks service refresh metadata

* Add tests for bookmarks api disable cache on check

* Remove bulk refresh metadata when background tasks disabled

* Refactor refresh metadata task

* Remove unnecessary call

* Fix testing mock name

* Abstract clearing metadata cache

* Add test to check if load page is called twice when cache disabled

* Remove refresh button for new bookmarks

* Remove strict disable cache is true check

* Refactor refresh metadata form logic into its own function

* move button and highlight changes

* polish and update tests

---------

Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
2025-03-22 11:34:10 +01:00
Jose Alvarez
f1acb4f7c9 Handle lowercase "true" in environment variables (#1020) 2025-03-22 08:27:35 +01:00
Sascha Ißbrücker
48fc499aed Add test for OIDC login link 2025-03-19 19:27:59 +01:00
Stefan Foerster
2a55800e18 Fix OIDC login link (#1019)
Fixes #1016.
2025-03-19 19:25:30 +01:00
Sascha Ißbrücker
e45dffb9cb Improve announcements after navigation (#1015) 2025-03-16 12:24:25 +01:00
Sascha Ißbrücker
226eb69f8b Accessibility improvements in page structure (#1014)
* Change app link to not use heading

* Use main and h1 for main content

* Update settings page structure

* Fix responsive styles

* Update bookmark form page structure

* Update auth page structure

* Add some basic page titles

* Expose side panel section

* Add page title for bookmark details

* Expose more sections

* Improve region names
2025-03-16 10:25:01 +01:00
Sascha Ißbrücker
b9bee24047 Move more logic into bookmark form 2025-03-09 23:11:48 +01:00
Sascha Ißbrücker
9dfc9b03b4 Fix E2E job 2025-03-09 12:30:10 +01:00
Sascha Ißbrücker
6ab6a031c7 Extract access checks 2025-03-09 12:21:22 +01:00
Sascha Ißbrücker
1a1092d03a Fix some type hints 2025-03-09 11:30:13 +01:00
Sascha Ißbrücker
4260dfce79 Remove duplicate URL calls in settings nav 2025-03-09 05:53:55 +01:00
Sascha Ißbrücker
2d3bd13a12 Merge siteroot application 2025-03-09 05:50:05 +01:00
Sascha Ißbrücker
b037de14c9 Move e2e tests 2025-03-09 05:46:26 +01:00
Sascha Ißbrücker
bbf173c135 Remove some duplication in bookmark routes 2025-03-09 05:45:50 +01:00
dependabot[bot]
002fec37d0 Bump django from 5.1.5 to 5.1.7 (#1007)
Bumps [django](https://github.com/django/django) from 5.1.5 to 5.1.7.
- [Commits](https://github.com/django/django/compare/5.1.5...5.1.7)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-08 09:21:10 +01:00
Sascha Ißbrücker
996e2b6e19 Add docs for auto tagging (#1009) 2025-03-08 09:20:35 +01:00
Sascha Ißbrücker
6838e45e99 Update api.md 2025-03-06 21:44:10 +01:00
151 changed files with 4696 additions and 3480 deletions

View File

@@ -3,7 +3,6 @@
# Include files required for build or at runtime
!/bookmarks
!/siteroot
!/bootstrap.sh
!/LICENSE.txt
@@ -19,4 +18,4 @@
!/version.txt
# Remove dev settings
/siteroot/settings/dev.py
/bookmarks/settings/dev.py

View File

@@ -19,6 +19,13 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -34,6 +41,8 @@ jobs:
tags: |
sissbruecker/linkding:latest
sissbruecker/linkding:${{ env.VERSION }}
ghcr.io/sissbruecker/linkding:latest
ghcr.io/sissbruecker/linkding:${{ env.VERSION }}
target: linkding
push: true
@@ -46,6 +55,8 @@ jobs:
tags: |
sissbruecker/linkding:latest-alpine
sissbruecker/linkding:${{ env.VERSION }}-alpine
ghcr.io/sissbruecker/linkding:latest-alpine
ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-alpine
target: linkding
push: true
@@ -58,6 +69,8 @@ jobs:
tags: |
sissbruecker/linkding:latest-plus
sissbruecker/linkding:${{ env.VERSION }}-plus
ghcr.io/sissbruecker/linkding:latest-plus
ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-plus
target: linkding-plus
push: true
@@ -70,5 +83,7 @@ jobs:
tags: |
sissbruecker/linkding:latest-plus-alpine
sissbruecker/linkding:${{ env.VERSION }}-plus-alpine
ghcr.io/sissbruecker/linkding:latest-plus-alpine
ghcr.io/sissbruecker/linkding:${{ env.VERSION }}-plus-alpine
target: linkding-plus
push: true

View File

@@ -55,4 +55,4 @@ jobs:
npm run build
python manage.py collectstatic
- name: Run tests
run: python manage.py test bookmarks.e2e --pattern="e2e_test_*.py"
run: python manage.py test bookmarks.tests_e2e --pattern="e2e_test_*.py"

View File

@@ -4,13 +4,12 @@ serve:
python manage.py runserver
tasks:
python manage.py process_tasks
python manage.py run_huey
test:
pytest -n auto
format:
black bookmarks
black siteroot
npx prettier bookmarks/frontend --write
npx prettier bookmarks/styles --write

View File

@@ -58,7 +58,7 @@ Small improvements, bugfixes and documentation improvements are always welcome.
## Development
The application is built using the Django web framework. You can get started by checking out the excellent [Django docs](https://docs.djangoproject.com/en/4.1/). The `bookmarks` folder contains the actual bookmark application, `siteroot` is the Django root application. Other than that the code should be self-explanatory / standard Django stuff 🙂.
The application is built using the Django web framework. You can get started by checking out the excellent [Django docs](https://docs.djangoproject.com/en/4.1/). The `bookmarks` folder contains the actual bookmark application. Other than that the code should be self-explanatory / standard Django stuff 🙂.
### Prerequisites
- Python 3.12

View File

@@ -3,7 +3,7 @@ import logging
import os
from django.conf import settings
from django.http import FileResponse, Http404
from django.http import Http404, StreamingHttpResponse
from rest_framework import viewsets, mixins, status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
@@ -19,6 +19,8 @@ from bookmarks.api.serializers import (
)
from bookmarks.models import Bookmark, BookmarkAsset, BookmarkSearch, Tag, User
from bookmarks.services import assets, bookmarks, auto_tagging, website_loader
from bookmarks.type_defs import HttpRequest
from bookmarks.views import access
logger = logging.getLogger(__name__)
@@ -31,6 +33,7 @@ class BookmarkViewSet(
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
):
request: HttpRequest
serializer_class = BookmarkSerializer
def get_permissions(self):
@@ -45,13 +48,21 @@ class BookmarkViewSet(
return super().get_permissions()
def get_queryset(self):
# Provide filtered queryset for list actions
user = self.request.user
# For list action, use query set that applies search and tag projections
search = BookmarkSearch.from_request(self.request.GET)
if self.action == "list":
search = BookmarkSearch.from_request(self.request.GET)
return queries.query_bookmarks(user, user.profile, search)
elif self.action == "archived":
return queries.query_archived_bookmarks(user, user.profile, search)
elif self.action == "shared":
user = User.objects.filter(username=search.user).first()
public_only = not self.request.user.is_authenticated
return queries.query_shared_bookmarks(
user, self.request.user_profile, search, public_only
)
# For single entity actions use default query set without projections
# For single entity actions return user owned bookmarks
return Bookmark.objects.all().filter(owner=user)
def get_serializer_context(self):
@@ -65,49 +76,35 @@ class BookmarkViewSet(
}
@action(methods=["get"], detail=False)
def archived(self, request):
user = request.user
search = BookmarkSearch.from_request(request.GET)
query_set = queries.query_archived_bookmarks(user, user.profile, search)
page = self.paginate_queryset(query_set)
serializer = self.get_serializer(page, many=True)
data = serializer.data
return self.get_paginated_response(data)
def archived(self, request: HttpRequest):
return self.list(request)
@action(methods=["get"], detail=False)
def shared(self, request):
search = BookmarkSearch.from_request(request.GET)
user = User.objects.filter(username=search.user).first()
public_only = not request.user.is_authenticated
query_set = queries.query_shared_bookmarks(
user, request.user_profile, search, public_only
)
page = self.paginate_queryset(query_set)
serializer = self.get_serializer(page, many=True)
data = serializer.data
return self.get_paginated_response(data)
def shared(self, request: HttpRequest):
return self.list(request)
@action(methods=["post"], detail=True)
def archive(self, request, pk):
def archive(self, request: HttpRequest, pk):
bookmark = self.get_object()
bookmarks.archive_bookmark(bookmark)
return Response(status=status.HTTP_204_NO_CONTENT)
@action(methods=["post"], detail=True)
def unarchive(self, request, pk):
def unarchive(self, request: HttpRequest, pk):
bookmark = self.get_object()
bookmarks.unarchive_bookmark(bookmark)
return Response(status=status.HTTP_204_NO_CONTENT)
@action(methods=["get"], detail=False)
def check(self, request):
def check(self, request: HttpRequest):
url = request.GET.get("url")
ignore_cache = request.GET.get("ignore_cache", False) in ["true"]
bookmark = Bookmark.objects.filter(owner=request.user, url=url).first()
existing_bookmark_data = (
self.get_serializer(bookmark).data if bookmark else None
)
metadata = website_loader.load_website_metadata(url)
metadata = website_loader.load_website_metadata(url, ignore_cache=ignore_cache)
# Return tags that would be automatically applied to the bookmark
profile = request.user.profile
@@ -131,13 +128,13 @@ class BookmarkViewSet(
)
@action(methods=["post"], detail=False)
def singlefile(self, request):
def singlefile(self, request: HttpRequest):
if settings.LD_DISABLE_ASSET_UPLOAD:
return Response(
{"error": "Asset upload is disabled."},
status=status.HTTP_403_FORBIDDEN,
)
url = request.data.get("url")
url = request.POST.get("url")
file = request.FILES.get("file")
if not url or not file:
@@ -169,22 +166,22 @@ class BookmarkAssetViewSet(
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
):
request: HttpRequest
serializer_class = BookmarkAssetSerializer
def get_queryset(self):
user = self.request.user
bookmark_id = self.kwargs["bookmark_id"]
if not Bookmark.objects.filter(id=bookmark_id, owner=user).exists():
raise Http404("Bookmark does not exist")
# limit access to assets to the owner of the bookmark for now
bookmark = access.bookmark_write(self.request, self.kwargs["bookmark_id"])
return BookmarkAsset.objects.filter(
bookmark_id=bookmark_id, bookmark__owner=user
bookmark_id=bookmark.id, bookmark__owner=user
)
def get_serializer_context(self):
return {"user": self.request.user}
@action(detail=True, methods=["get"], url_path="download")
def download(self, request, bookmark_id, pk):
def download(self, request: HttpRequest, bookmark_id, pk):
asset = self.get_object()
try:
file_path = os.path.join(settings.LD_ASSET_FOLDER, asset.file)
@@ -199,7 +196,7 @@ class BookmarkAssetViewSet(
if asset.asset_type == BookmarkAsset.TYPE_SNAPSHOT
else asset.display_name
)
response = FileResponse(file_stream, content_type=content_type)
response = StreamingHttpResponse(file_stream, content_type=content_type)
response["Content-Disposition"] = f'attachment; filename="{file_name}"'
return response
except FileNotFoundError:
@@ -212,15 +209,13 @@ class BookmarkAssetViewSet(
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(methods=["post"], detail=False)
def upload(self, request, bookmark_id):
def upload(self, request: HttpRequest, bookmark_id):
if settings.LD_DISABLE_ASSET_UPLOAD:
return Response(
{"error": "Asset upload is disabled."},
status=status.HTTP_403_FORBIDDEN,
)
bookmark = Bookmark.objects.filter(id=bookmark_id, owner=request.user).first()
if not bookmark:
raise Http404("Bookmark does not exist")
bookmark = access.bookmark_write(request, bookmark_id)
upload_file = request.FILES.get("file")
if not upload_file:
@@ -242,6 +237,9 @@ class BookmarkAssetViewSet(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def perform_destroy(self, instance):
assets.remove_asset(instance)
class TagViewSet(
viewsets.GenericViewSet,
@@ -249,6 +247,7 @@ class TagViewSet(
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
):
request: HttpRequest
serializer_class = TagSerializer
def get_queryset(self):
@@ -261,7 +260,7 @@ class TagViewSet(
class UserViewSet(viewsets.GenericViewSet):
@action(methods=["get"], detail=False)
def profile(self, request):
def profile(self, request: HttpRequest):
return Response(UserProfileSerializer(request.user.profile).data)

View File

@@ -22,6 +22,11 @@ class BookmarkListSerializer(ListSerializer):
return super().to_representation(data)
class EmtpyField(serializers.ReadOnlyField):
def to_representation(self, value):
return None
class BookmarkSerializer(serializers.ModelSerializer):
class Meta:
model = Bookmark
@@ -62,8 +67,8 @@ class BookmarkSerializer(serializers.ModelSerializer):
preview_image_url = serializers.SerializerMethodField()
web_archive_snapshot_url = serializers.SerializerMethodField()
# Add dummy website title and description fields for backwards compatibility but keep them empty
website_title = serializers.SerializerMethodField()
website_description = serializers.SerializerMethodField()
website_title = EmtpyField()
website_description = EmtpyField()
def get_favicon_url(self, obj: Bookmark):
if not obj.favicon_file:
@@ -87,12 +92,6 @@ class BookmarkSerializer(serializers.ModelSerializer):
return generate_fallback_webarchive_url(obj.url, obj.date_added)
def get_website_title(self, obj: Bookmark):
return None
def get_website_description(self, obj: Bookmark):
return None
def create(self, validated_data):
tag_names = validated_data.pop("tag_names", [])
tag_string = build_tag_string(tag_names)
@@ -185,9 +184,5 @@ class UserProfileSerializer(serializers.ModelSerializer):
"search_preferences",
"version",
]
read_only_fields = ["version"]
version = serializers.SerializerMethodField()
def get_version(self, obj: UserProfile):
return app_version
version = serializers.ReadOnlyField(default=app_version)

View File

@@ -1,6 +1,5 @@
from bookmarks import queries
from bookmarks.models import BookmarkSearch, Toast
from bookmarks import utils
from bookmarks.models import Toast
def toasts(request):

View File

@@ -1,166 +0,0 @@
from unittest.mock import patch
from urllib.parse import quote
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from bookmarks.e2e.helpers import LinkdingE2ETestCase
from bookmarks.services import website_loader
mock_website_metadata = website_loader.WebsiteMetadata(
url="https://example.com",
title="Example Domain",
description="This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.",
preview_image=None,
)
class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
def setUp(self) -> None:
super().setUp()
self.website_loader_patch = patch.object(
website_loader, "load_website_metadata", return_value=mock_website_metadata
)
self.website_loader_patch.start()
def tearDown(self) -> None:
super().tearDown()
self.website_loader_patch.stop()
def test_enter_url_prefills_title_and_description(self):
with sync_playwright() as p:
page = self.open(reverse("bookmarks:new"), p)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
url.fill("https://example.com")
expect(title).to_have_value("Example Domain")
expect(description).to_have_value(
"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission."
)
def test_enter_url_does_not_overwrite_modified_title_and_description(self):
with sync_playwright() as p:
page = self.open(reverse("bookmarks:new"), p)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
title.fill("Modified title")
description.fill("Modified description")
url.fill("https://example.com")
page.wait_for_timeout(timeout=1000)
expect(title).to_have_value("Modified title")
expect(description).to_have_value("Modified description")
def test_with_initial_url_prefills_title_and_description(self):
with sync_playwright() as p:
page_url = reverse("bookmarks:new") + f"?url={quote('https://example.com')}"
page = self.open(page_url, p)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
page.wait_for_timeout(timeout=1000)
expect(url).to_have_value("https://example.com")
expect(title).to_have_value("Example Domain")
expect(description).to_have_value(
"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission."
)
def test_with_initial_url_title_description_does_not_overwrite_title_and_description(
self,
):
with sync_playwright() as p:
page_url = (
reverse("bookmarks:new")
+ f"?url={quote('https://example.com')}&title=Initial+title&description=Initial+description"
)
page = self.open(page_url, p)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
page.wait_for_timeout(timeout=1000)
expect(url).to_have_value("https://example.com")
expect(title).to_have_value("Initial title")
expect(description).to_have_value("Initial description")
def test_create_should_check_for_existing_bookmark(self):
existing_bookmark = self.setup_bookmark(
title="Existing title",
description="Existing description",
notes="Existing notes",
tags=[self.setup_tag(name="tag1"), self.setup_tag(name="tag2")],
unread=True,
)
tag_names = " ".join(existing_bookmark.tag_names)
with sync_playwright() as p:
page = self.open(reverse("bookmarks:new"), p)
# Enter bookmarked URL
page.get_by_label("URL").fill(existing_bookmark.url)
# Already bookmarked hint should be visible
page.get_by_text("This URL is already bookmarked.").wait_for(timeout=2000)
# Form should be pre-filled with data from existing bookmark
self.assertEqual(
existing_bookmark.title, page.get_by_label("Title").input_value()
)
self.assertEqual(
existing_bookmark.description,
page.get_by_label("Description").input_value(),
)
self.assertEqual(
existing_bookmark.notes, page.get_by_label("Notes").input_value()
)
self.assertEqual(tag_names, page.get_by_label("Tags").input_value())
self.assertTrue(tag_names, page.get_by_label("Mark as unread").is_checked())
# Enter non-bookmarked URL
page.get_by_label("URL").fill("https://example.com/unknown")
# Already bookmarked hint should be hidden
page.get_by_text("This URL is already bookmarked.").wait_for(
state="hidden", timeout=2000
)
def test_enter_url_of_existing_bookmark_should_show_notes(self):
bookmark = self.setup_bookmark(
notes="Existing notes", description="Existing description"
)
with sync_playwright() as p:
page = self.open(reverse("bookmarks:new"), p)
details = page.locator("details.notes")
expect(details).not_to_have_attribute("open", value="")
page.get_by_label("URL").fill(bookmark.url)
expect(details).to_have_attribute("open", value="")
def test_create_should_preview_auto_tags(self):
profile = self.get_or_create_test_user().profile
profile.auto_tagging_rules = "github.com dev github"
profile.save()
with sync_playwright() as p:
# Open page with URL that should have auto tags
url = (
reverse("bookmarks:new")
+ "?url=https%3A%2F%2Fgithub.com%2Fsissbruecker%2Flinkding"
)
page = self.open(url, p)
auto_tags_hint = page.locator(".form-input-hint.auto-tags")
expect(auto_tags_hint).to_be_visible()
expect(auto_tags_hint).to_have_text("Auto tags: dev github")
# Change to URL without auto tags
page.get_by_label("URL").fill("https://example.com")
expect(auto_tags_hint).to_be_hidden()

View File

@@ -74,7 +74,7 @@ class AllBookmarksFeed(BaseBookmarksFeed):
return queries.query_bookmarks(feed_token.user, feed_token.user.profile, search)
def link(self, context: FeedContext):
return reverse("bookmarks:feeds.all", args=[context.feed_token.key])
return reverse("linkding:feeds.all", args=[context.feed_token.key])
class UnreadBookmarksFeed(BaseBookmarksFeed):
@@ -87,7 +87,7 @@ class UnreadBookmarksFeed(BaseBookmarksFeed):
).filter(unread=True)
def link(self, context: FeedContext):
return reverse("bookmarks:feeds.unread", args=[context.feed_token.key])
return reverse("linkding:feeds.unread", args=[context.feed_token.key])
class SharedBookmarksFeed(BaseBookmarksFeed):
@@ -100,7 +100,7 @@ class SharedBookmarksFeed(BaseBookmarksFeed):
)
def link(self, context: FeedContext):
return reverse("bookmarks:feeds.shared", args=[context.feed_token.key])
return reverse("linkding:feeds.shared", args=[context.feed_token.key])
class PublicSharedBookmarksFeed(BaseBookmarksFeed):
@@ -114,4 +114,4 @@ class PublicSharedBookmarksFeed(BaseBookmarksFeed):
return queries.query_shared_bookmarks(None, UserProfile(), search, True)
def link(self, context: FeedContext):
return reverse("bookmarks:feeds.public_shared")
return reverse("linkding:feeds.public_shared")

95
bookmarks/forms.py Normal file
View File

@@ -0,0 +1,95 @@
from django import forms
from bookmarks.models import Bookmark, build_tag_string
from bookmarks.validators import BookmarkURLValidator
from bookmarks.type_defs import HttpRequest
from bookmarks.services.bookmarks import create_bookmark, update_bookmark
class BookmarkForm(forms.ModelForm):
# Use URLField for URL
url = forms.CharField(validators=[BookmarkURLValidator()])
tag_string = forms.CharField(required=False)
# Do not require title and description as they may be empty
title = forms.CharField(max_length=512, required=False)
description = forms.CharField(required=False, widget=forms.Textarea())
unread = forms.BooleanField(required=False)
shared = forms.BooleanField(required=False)
# Hidden field that determines whether to close window/tab after saving the bookmark
auto_close = forms.CharField(required=False)
class Meta:
model = Bookmark
fields = [
"url",
"tag_string",
"title",
"description",
"notes",
"unread",
"shared",
"auto_close",
]
def __init__(self, request: HttpRequest, instance: Bookmark = None):
self.request = request
initial = None
if instance is None and request.method == "GET":
initial = {
"url": request.GET.get("url"),
"title": request.GET.get("title"),
"description": request.GET.get("description"),
"notes": request.GET.get("notes"),
"tag_string": request.GET.get("tags"),
"auto_close": "auto_close" in request.GET,
"unread": request.user_profile.default_mark_unread,
}
if instance is not None and request.method == "GET":
initial = {"tag_string": build_tag_string(instance.tag_names, " ")}
data = request.POST if request.method == "POST" else None
super().__init__(data, instance=instance, initial=initial)
@property
def is_auto_close(self):
return self.data.get("auto_close", False) == "True" or self.initial.get(
"auto_close", False
)
@property
def has_notes(self):
return self.initial.get("notes", None) or (
self.instance and self.instance.notes
)
def save(self, commit=False):
tag_string = convert_tag_string(self.data["tag_string"])
bookmark = super().save(commit=False)
if self.instance.pk:
return update_bookmark(bookmark, tag_string, self.request.user)
else:
return create_bookmark(bookmark, tag_string, self.request.user)
def clean_url(self):
# When creating a bookmark, the service logic prevents duplicate URLs by
# updating the existing bookmark instead, which is also communicated in
# the form's UI. When editing a bookmark, there is no assumption that
# it would update a different bookmark if the URL is a duplicate, so
# raise a validation error in that case.
url = self.cleaned_data["url"]
if self.instance.pk:
is_duplicate = (
Bookmark.objects.filter(owner=self.instance.owner, url=url)
.exclude(pk=self.instance.pk)
.exists()
)
if is_duplicate:
raise forms.ValidationError("A bookmark with this URL already exists.")
return url
def convert_tag_string(tag_string: str):
# Tag strings coming from inputs are space-separated, however services.bookmarks functions expect comma-separated
# strings
return tag_string.replace(" ", ",")

View File

@@ -1,5 +1,5 @@
import { registerBehavior } from "./index";
import { isKeyboardActive } from "./focus-utils";
import { isKeyboardActive, setAfterPageLoadFocusTarget } from "./focus-utils";
import { ModalBehavior } from "./modal";
class DetailsModalBehavior extends ModalBehavior {
@@ -15,14 +15,9 @@ class DetailsModalBehavior extends ModalBehavior {
// Try restore focus to view details to view details link of respective bookmark
const bookmarkId = this.element.dataset.bookmarkId;
const restoreFocusElement =
document.querySelector(
`ul.bookmark-list li[data-bookmark-id='${bookmarkId}'] a.view-action`,
) ||
document.querySelector("ul.bookmark-list") ||
document.body;
restoreFocusElement.focus({ focusVisible: isKeyboardActive() });
setAfterPageLoadFocusTarget(
`ul.bookmark-list li[data-bookmark-id='${bookmarkId}'] a.view-action`,
);
}
}

View File

@@ -34,7 +34,7 @@ class FilterDrawerTriggerBehavior extends Behavior {
</button>
</div>
<div class="modal-body">
<section class="content content-area"></div>
<div class="content"></div>
</div>
</div>
`;
@@ -70,13 +70,13 @@ class FilterDrawerBehavior extends ModalBehavior {
teleport() {
const content = this.element.querySelector(".content");
const sidePanel = document.querySelector("section.side-panel");
const sidePanel = document.querySelector(".side-panel");
content.append(...sidePanel.children);
this.mapHeading(content, "h2", "h3");
}
teleportBack() {
const sidePanel = document.querySelector("section.side-panel");
const sidePanel = document.querySelector(".side-panel");
const content = this.element.querySelector(".content");
sidePanel.append(...content.children);
this.mapHeading(sidePanel, "h3", "h2");

View File

@@ -57,3 +57,68 @@ export class FocusTrapController {
}
}
}
let afterPageLoadFocusTarget = [];
let firstPageLoad = true;
export function setAfterPageLoadFocusTarget(...targets) {
afterPageLoadFocusTarget = targets;
}
function programmaticFocus(element) {
// Ensure element is focusable
// Hide focus outline if element is not focusable by default - might
// reconsider this later
const isFocusable = element.tabIndex >= 0;
if (!isFocusable) {
// Apparently the default tabIndex is -1, even though an element is still
// not focusable with that. Setting an explicit -1 also sets the attribute
// and the element becomes focusable.
element.tabIndex = -1;
// `focusVisible` is not supported in all browsers, so hide the outline manually
element.style["outline"] = "none";
}
element.focus({
focusVisible: isKeyboardActive() && isFocusable,
preventScroll: true,
});
}
// Register global listener for navigation and try to focus an element that
// results in a meaningful announcement.
document.addEventListener("turbo:load", () => {
// Ignore initial page load to let the browser handle announcements
if (firstPageLoad) {
firstPageLoad = false;
return;
}
// Check if there is an explicit focus target for the next page load
for (const target of afterPageLoadFocusTarget) {
const element = document.querySelector(target);
if (element) {
programmaticFocus(element);
return;
}
}
afterPageLoadFocusTarget = [];
// If there is some autofocus element, let the browser handle it
const autofocus = document.querySelector("[autofocus]");
if (autofocus) {
return;
}
// If there is a toast as a result of some action, focus it
const toast = document.querySelector(".toast");
if (toast) {
programmaticFocus(toast);
return;
}
// Otherwise go with main
const main = document.querySelector("main");
if (main) {
programmaticFocus(main);
}
});

View File

@@ -0,0 +1,46 @@
# Generated by Django 5.1.7 on 2025-03-22 12:28
import django.db.models.deletion
from django.db import migrations, models
from django.db.models import OuterRef, Subquery
def forwards(apps, schema_editor):
# Update the latest snapshot for each bookmark
Bookmark = apps.get_model("bookmarks", "bookmark")
BookmarkAsset = apps.get_model("bookmarks", "bookmarkasset")
latest_snapshots = (
BookmarkAsset.objects.filter(
bookmark=OuterRef("pk"), asset_type="snapshot", status="complete"
)
.order_by("-date_created")
.values("id")[:1]
)
Bookmark.objects.update(latest_snapshot_id=Subquery(latest_snapshots))
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("bookmarks", "0043_userprofile_collapse_side_panel"),
]
operations = [
migrations.AddField(
model_name="bookmark",
name="latest_snapshot",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="latest_snapshot",
to="bookmarks.bookmarkasset",
),
),
migrations.RunPython(forwards, reverse),
]

View File

@@ -6,7 +6,6 @@ from typing import List
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator
from django.db import models
@@ -23,7 +22,7 @@ logger = logging.getLogger(__name__)
class Tag(models.Model):
name = models.CharField(max_length=64)
date_added = models.DateTimeField()
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.name
@@ -70,8 +69,15 @@ class Bookmark(models.Model):
date_added = models.DateTimeField()
date_modified = models.DateTimeField()
date_accessed = models.DateTimeField(blank=True, null=True)
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
latest_snapshot = models.ForeignKey(
"BookmarkAsset",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="latest_snapshot",
)
@property
def resolved_title(self):
@@ -151,56 +157,6 @@ def bookmark_asset_deleted(sender, instance, **kwargs):
logger.error(f"Failed to delete asset file: {filepath}", exc_info=error)
class BookmarkForm(forms.ModelForm):
# Use URLField for URL
url = forms.CharField(validators=[BookmarkURLValidator()])
tag_string = forms.CharField(required=False)
# Do not require title and description as they may be empty
title = forms.CharField(max_length=512, required=False)
description = forms.CharField(required=False, widget=forms.Textarea())
unread = forms.BooleanField(required=False)
shared = forms.BooleanField(required=False)
# Hidden field that determines whether to close window/tab after saving the bookmark
auto_close = forms.CharField(required=False)
class Meta:
model = Bookmark
fields = [
"url",
"tag_string",
"title",
"description",
"notes",
"unread",
"shared",
"auto_close",
]
@property
def has_notes(self):
return self.initial.get("notes", None) or (
self.instance and self.instance.notes
)
def clean_url(self):
# When creating a bookmark, the service logic prevents duplicate URLs by
# updating the existing bookmark instead, which is also communicated in
# the form's UI. When editing a bookmark, there is no assumption that
# it would update a different bookmark if the URL is a duplicate, so
# raise a validation error in that case.
url = self.cleaned_data["url"]
if self.instance.pk:
is_duplicate = (
Bookmark.objects.filter(owner=self.instance.owner, url=url)
.exclude(pk=self.instance.pk)
.exists()
)
if is_duplicate:
raise forms.ValidationError("A bookmark with this URL already exists.")
return url
class BookmarkSearch:
SORT_ADDED_ASC = "added_asc"
SORT_ADDED_DESC = "added_desc"
@@ -387,9 +343,7 @@ class UserProfile(models.Model):
(TAG_GROUPING_ALPHABETICAL, "Alphabetical"),
(TAG_GROUPING_DISABLED, "Disabled"),
]
user = models.OneToOneField(
get_user_model(), related_name="profile", on_delete=models.CASCADE
)
user = models.OneToOneField(User, related_name="profile", on_delete=models.CASCADE)
theme = models.CharField(
max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO
)
@@ -497,13 +451,13 @@ class UserProfileForm(forms.ModelForm):
]
@receiver(post_save, sender=get_user_model())
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=get_user_model())
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
@@ -512,7 +466,7 @@ class Toast(models.Model):
key = models.CharField(max_length=50)
message = models.TextField()
acknowledged = models.BooleanField(default=False)
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
class FeedToken(models.Model):
@@ -522,7 +476,7 @@ class FeedToken(models.Model):
key = models.CharField(max_length=40, primary_key=True)
user = models.OneToOneField(
get_user_model(),
User,
related_name="feed_token",
on_delete=models.CASCADE,
)
@@ -556,7 +510,7 @@ class GlobalSettings(models.Model):
default=LANDING_PAGE_LOGIN,
)
guest_profile_user = models.ForeignKey(
get_user_model(), on_delete=models.SET_NULL, null=True, blank=True
User, on_delete=models.SET_NULL, null=True, blank=True
)
enable_link_prefetch = models.BooleanField(default=False, null=False)

View File

@@ -51,6 +51,9 @@ def create_snapshot(asset: BookmarkAsset):
asset.file = filename
asset.gzip = True
asset.save()
asset.bookmark.latest_snapshot = asset
asset.bookmark.save()
except Exception as error:
asset.status = BookmarkAsset.STATUS_FAILURE
asset.save()
@@ -71,6 +74,9 @@ def upload_snapshot(bookmark: Bookmark, html: bytes):
asset.gzip = True
asset.save()
asset.bookmark.latest_snapshot = asset
asset.bookmark.save()
return asset
@@ -106,6 +112,27 @@ def upload_asset(bookmark: Bookmark, upload_file: UploadedFile):
raise e
def remove_asset(asset: BookmarkAsset):
# If this asset is the latest_snapshot for a bookmark, try to find the next most recent snapshot
bookmark = asset.bookmark
if bookmark and bookmark.latest_snapshot == asset:
latest = (
BookmarkAsset.objects.filter(
bookmark=bookmark,
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
status=BookmarkAsset.STATUS_COMPLETE,
)
.exclude(pk=asset.pk)
.order_by("-date_created")
.first()
)
bookmark.latest_snapshot = latest
bookmark.save()
asset.delete()
def _generate_asset_filename(
asset: BookmarkAsset, filename: str, extension: str
) -> str:

View File

@@ -11,10 +11,18 @@ def get_tags(script: str, url: str):
return result
for line in script.lower().split("\n"):
if "#" in line:
i = line.index("#")
line = line[:i]
line = line.strip()
# Skip empty lines or lines that start with a comment
if not line or line.startswith("#"):
continue
# Remove trailing comment - only if # is preceded by whitespace
comment_match = re.search(r"\s+#", line)
if comment_match:
line = line[: comment_match.start()]
# Ignore lines that don't contain a URL and a tag
parts = line.split()
if len(parts) < 2:
continue
@@ -36,6 +44,11 @@ def get_tags(script: str, url: str):
):
continue
if parsed_pattern.fragment and not _fragment_matches(
parsed_pattern.fragment, parsed_url.fragment
):
continue
for tag in parts[1:]:
result.add(tag)
@@ -65,3 +78,7 @@ def _qs_matches(expected_qs: str, actual_qs: str) -> bool:
return False
return True
def _fragment_matches(expected_fragment: str, actual_fragment: str) -> bool:
return actual_fragment.startswith(expected_fragment)

View File

@@ -1,10 +1,9 @@
import logging
from typing import Union
from django.contrib.auth.models import User
from django.utils import timezone
from bookmarks.models import Bookmark, parse_tag_string
from bookmarks.models import Bookmark, User, parse_tag_string
from bookmarks.services import auto_tagging
from bookmarks.services import tasks
from bookmarks.services import website_loader
@@ -198,6 +197,17 @@ def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
)
def refresh_bookmarks_metadata(bookmark_ids: [Union[int, str]], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
owned_bookmarks = Bookmark.objects.filter(
owner=current_user, id__in=sanitized_bookmark_ids
)
for bookmark in owned_bookmarks:
tasks.refresh_metadata(bookmark)
tasks.load_preview_image(current_user, bookmark)
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
to_bookmark.title = from_bookmark.title
to_bookmark.description = from_bookmark.description

View File

@@ -35,7 +35,7 @@ def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
desc += f"[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]"
tag_names = bookmark.tag_names
if bookmark.is_archived:
tag_names.append("linkding:archived")
tag_names.append("linkding:bookmarks.archived")
tags = ",".join(tag_names)
toread = "1" if bookmark.unread else "0"
private = "0" if bookmark.shared else "1"

View File

@@ -62,9 +62,9 @@ class BookmarkParser(HTMLParser):
def handle_start_a(self, attrs: Dict[str, str]):
vars(self).update(attrs)
tag_names = parse_tag_string(self.tags)
archived = "linkding:archived" in self.tags
archived = "linkding:bookmarks.archived" in self.tags
try:
tag_names.remove("linkding:archived")
tag_names.remove("linkding:bookmarks.archived")
except ValueError:
pass

View File

@@ -4,9 +4,9 @@ from typing import List
import waybackpy
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.db.models import Q
from django.utils import timezone
from huey import crontab
from huey.contrib.djhuey import HUEY as huey
from huey.exceptions import TaskLockedException
@@ -14,7 +14,7 @@ from waybackpy.exceptions import WaybackError, TooManyRequestsError
from bookmarks.models import Bookmark, BookmarkAsset, UserProfile
from bookmarks.services import assets, favicon_loader, preview_image_loader
from bookmarks.services.website_loader import DEFAULT_USER_AGENT
from bookmarks.services.website_loader import DEFAULT_USER_AGENT, load_website_metadata
logger = logging.getLogger(__name__)
@@ -157,7 +157,7 @@ def schedule_bookmarks_without_favicons(user: User):
@task()
def _schedule_bookmarks_without_favicons_task(user_id: int):
user = get_user_model().objects.get(id=user_id)
user = User.objects.get(id=user_id)
bookmarks = Bookmark.objects.filter(favicon_file__exact="", owner=user)
# TODO: Implement bulk task creation
@@ -173,7 +173,7 @@ def schedule_refresh_favicons(user: User):
@task()
def _schedule_refresh_favicons_task(user_id: int):
user = get_user_model().objects.get(id=user_id)
user = User.objects.get(id=user_id)
bookmarks = Bookmark.objects.filter(owner=user)
# TODO: Implement bulk task creation
@@ -212,7 +212,7 @@ def schedule_bookmarks_without_previews(user: User):
@task()
def _schedule_bookmarks_without_previews_task(user_id: int):
user = get_user_model().objects.get(id=user_id)
user = User.objects.get(id=user_id)
bookmarks = Bookmark.objects.filter(
Q(preview_image_file__exact=""),
owner=user,
@@ -226,6 +226,31 @@ def _schedule_bookmarks_without_previews_task(user_id: int):
logging.exception(exc)
def refresh_metadata(bookmark: Bookmark):
if not settings.LD_DISABLE_BACKGROUND_TASKS:
_refresh_metadata_task(bookmark.id)
@task()
def _refresh_metadata_task(bookmark_id: int):
try:
bookmark = Bookmark.objects.get(id=bookmark_id)
except Bookmark.DoesNotExist:
return
logger.info(f"Refresh metadata for bookmark. url={bookmark.url}")
metadata = load_website_metadata(bookmark.url)
if metadata.title:
bookmark.title = metadata.title
if metadata.description:
bookmark.description = metadata.description
bookmark.date_modified = timezone.now()
bookmark.save()
logger.info(f"Successfully refreshed metadata for bookmark. url={bookmark.url}")
def is_html_snapshot_feature_active() -> bool:
return settings.LD_ENABLE_SNAPSHOTS and not settings.LD_DISABLE_BACKGROUND_TASKS

View File

@@ -27,10 +27,20 @@ class WebsiteMetadata:
}
def load_website_metadata(url: str, ignore_cache: bool = False):
if ignore_cache:
return _load_website_metadata(url)
return _load_website_metadata_cached(url)
# Caching metadata avoids scraping again when saving bookmarks, in case the
# metadata was already scraped to show preview values in the bookmark form
@lru_cache(maxsize=10)
def load_website_metadata(url: str):
def _load_website_metadata_cached(url: str):
return _load_website_metadata(url)
def _load_website_metadata(url: str):
title = None
description = None
preview_image = None

View File

@@ -58,7 +58,7 @@ MIDDLEWARE = [
"django.middleware.locale.LocaleMiddleware",
]
ROOT_URLCONF = "siteroot.urls"
ROOT_URLCONF = "bookmarks.urls"
TEMPLATES = [
{
@@ -80,7 +80,7 @@ TEMPLATES = [
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
WSGI_APPLICATION = "siteroot.wsgi.application"
WSGI_APPLICATION = "bookmarks.wsgi.application"
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
@@ -146,6 +146,7 @@ ALLOW_REGISTRATION = False
LD_DISABLE_URL_VALIDATION = os.getenv("LD_DISABLE_URL_VALIDATION", False) in (
True,
"True",
"true",
"1",
)
@@ -153,6 +154,7 @@ LD_DISABLE_URL_VALIDATION = os.getenv("LD_DISABLE_URL_VALIDATION", False) in (
LD_DISABLE_BACKGROUND_TASKS = os.getenv("LD_DISABLE_BACKGROUND_TASKS", False) in (
True,
"True",
"true",
"1",
)
@@ -179,7 +181,7 @@ HUEY = {
# Enable OICD support if configured
LD_ENABLE_OIDC = os.getenv("LD_ENABLE_OIDC", False) in (True, "True", "1")
LD_ENABLE_OIDC = os.getenv("LD_ENABLE_OIDC", False) in (True, "True", "true", "1")
AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"]
@@ -195,12 +197,17 @@ if LD_ENABLE_OIDC:
OIDC_RP_CLIENT_SECRET = os.getenv("OIDC_RP_CLIENT_SECRET")
OIDC_RP_SIGN_ALGO = os.getenv("OIDC_RP_SIGN_ALGO", "RS256")
OIDC_RP_SCOPES = os.getenv("OIDC_RP_SCOPES", "openid email profile")
OIDC_USE_PKCE = os.getenv("OIDC_USE_PKCE", True) in (True, "True", "1")
OIDC_VERIFY_SSL = os.getenv("OIDC_VERIFY_SSL", True) in (True, "True", "1")
OIDC_USE_PKCE = os.getenv("OIDC_USE_PKCE", True) in (True, "True", "true", "1")
OIDC_VERIFY_SSL = os.getenv("OIDC_VERIFY_SSL", True) in (True, "True", "true", "1")
OIDC_USERNAME_CLAIM = os.getenv("OIDC_USERNAME_CLAIM", "email")
# Enable authentication proxy support if configured
LD_ENABLE_AUTH_PROXY = os.getenv("LD_ENABLE_AUTH_PROXY", False) in (True, "True", "1")
LD_ENABLE_AUTH_PROXY = os.getenv("LD_ENABLE_AUTH_PROXY", False) in (
True,
"True",
"true",
"1",
)
LD_AUTH_PROXY_USERNAME_HEADER = os.getenv(
"LD_AUTH_PROXY_USERNAME_HEADER", "REMOTE_USER"
)
@@ -267,6 +274,7 @@ LD_FAVICON_FOLDER = os.path.join(BASE_DIR, "data", "favicons")
LD_ENABLE_REFRESH_FAVICONS = os.getenv("LD_ENABLE_REFRESH_FAVICONS", True) in (
True,
"True",
"true",
"1",
)
@@ -288,11 +296,13 @@ LD_ASSET_FOLDER = os.path.join(BASE_DIR, "data", "assets")
LD_ENABLE_SNAPSHOTS = os.getenv("LD_ENABLE_SNAPSHOTS", False) in (
True,
"True",
"true",
"1",
)
LD_DISABLE_ASSET_UPLOAD = os.getenv("LD_DISABLE_ASSET_UPLOAD", False) in (
True,
"True",
"true",
"1",
)
LD_SINGLEFILE_PATH = os.getenv("LD_SINGLEFILE_PATH", "single-file")

View File

@@ -1,5 +1,5 @@
.bookmarks-form-page {
section {
main {
max-width: 550px;
margin: 0 auto;
}
@@ -15,14 +15,23 @@
visibility: hidden;
}
& .form-group .clear-button {
display: none;
& .form-group .suffix-button {
padding: 0;
border: none;
height: auto;
font-size: var(--font-size-sm);
}
& .form-group .clear-button,
& .form-group #refresh-button {
display: none;
}
& .form-group input.modified,
& .form-group textarea.modified {
background: var(--primary-color-shade);
}
& .form-input-hint.bookmark-exists {
display: none;
color: var(--warning-color);

View File

@@ -30,11 +30,11 @@
}
&.collapse-side-panel {
section.main {
main {
grid-column: span var(--grid-columns);
}
section.side-panel {
.side-panel {
display: none;
}
@@ -459,7 +459,7 @@ ul.bookmark-list {
/* Hide section border when bulk edit bar is opened, otherwise borders overlap in dark mode due to using contrast colors */
&.active section:first-of-type .content-area-header {
&.active .main .section-header {
border-bottom-color: transparent;
}

View File

@@ -1,36 +1,31 @@
/* Shared components */
/* Content area component */
section.content-area {
/* Section header component */
.section-header {
border-bottom: solid 1px var(--secondary-border-color);
display: flex;
flex-wrap: wrap;
column-gap: var(--unit-5);
padding-bottom: var(--unit-2);
margin-bottom: var(--unit-4);
h1,
h2,
h3 {
font-size: var(--font-size-lg);
flex: 0 0 auto;
line-height: var(--unit-9);
margin: 0;
}
.content-area-header {
border-bottom: solid 1px var(--secondary-border-color);
.header-controls {
flex: 1 1 0;
display: flex;
flex-wrap: wrap;
column-gap: var(--unit-5);
padding-bottom: var(--unit-2);
margin-bottom: var(--unit-4);
h2,
h3 {
flex: 0 0 auto;
line-height: var(--unit-9);
margin: 0;
}
.header-controls {
flex: 1 1 0;
display: flex;
}
}
}
@media (max-width: 600px) {
section.content-area .content-area-header {
.section-header {
flex-direction: column;
}
}

View File

@@ -11,18 +11,20 @@ body {
header {
margin-bottom: var(--unit-9);
.logo {
a.app-link:hover {
text-decoration: none;
}
.app-logo {
width: 28px;
height: 28px;
}
a:hover {
text-decoration: none;
}
h1 {
margin: 0 0 0 var(--unit-3);
.app-name {
margin-left: var(--unit-3);
font-size: var(--font-size-lg);
font-weight: 500;
line-height: 1.2;
}
}

View File

@@ -1,8 +1,14 @@
.settings-page {
section.content-area {
h1 {
font-size: var(--font-size-xl);
margin-bottom: var(--unit-6);
}
section {
margin-bottom: var(--unit-10);
h2 {
font-size: var(--font-size-lg);
margin-bottom: var(--unit-3);
}
}

View File

@@ -87,6 +87,7 @@
--font-size: 0.7rem;
--font-size-sm: 0.65rem;
--font-size-lg: 0.8rem;
--font-size-xl: 1rem;
--line-height: 1rem;
/* Sizes */

View File

@@ -8,9 +8,9 @@
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #}
<section class="main content-area col-2">
<div class="content-area-header mb-0">
<h2>Archived bookmarks</h2>
<main class="main col-2" aria-labelledby="main-heading">
<div class="section-header mb-0">
<h1 id="main-heading">Archived bookmarks</h1>
<div class="header-controls">
{% bookmark_search bookmark_list.search mode='archived' %}
{% include 'bookmarks/bulk_edit/toggle.html' %}
@@ -28,17 +28,19 @@
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form>
</section>
</main>
{# Tag cloud #}
<section class="side-panel content-area col-1">
<div class="content-area-header">
<h2>Tags</h2>
</div>
<div id="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
<div class="side-panel col-1">
<section aria-labelledby="tags-heading">
<div class="section-header">
<h2 id="tags-heading">Tags</h2>
</div>
<div id="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
</div>
</div>
{% endblock %}

View File

@@ -5,160 +5,162 @@
{% if bookmark_list.is_empty %}
{% include 'bookmarks/empty_bookmarks.html' %}
{% else %}
<ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}"
role="list" tabindex="-1"
style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }};"
data-bookmarks-total="{{ bookmark_list.bookmarks_total }}">
{% for bookmark_item in bookmark_list.items %}
<li ld-bookmark-item data-bookmark-id="{{ bookmark_item.id }}" role="listitem"
{% if bookmark_item.css_classes %}class="{{ bookmark_item.css_classes }}"{% endif %}>
<div class="content">
<div class="title">
<label class="form-checkbox bulk-edit-checkbox">
<input type="checkbox" name="bookmark_id" value="{{ bookmark_item.id }}">
<i class="form-icon"></i>
</label>
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
<img class="favicon" src="{% static bookmark_item.favicon_file %}" alt="">
{% endif %}
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener">
<span>{{ bookmark_item.title }}</span>
</a>
</div>
{% if bookmark_list.show_url %}
<div class="url-path truncate">
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
class="url-display">
{{ bookmark_item.url }}
<section aria-label="Bookmark list">
<ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}"
role="list" tabindex="-1"
style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }};"
data-bookmarks-total="{{ bookmark_list.bookmarks_total }}">
{% for bookmark_item in bookmark_list.items %}
<li ld-bookmark-item data-bookmark-id="{{ bookmark_item.id }}" role="listitem"
{% if bookmark_item.css_classes %}class="{{ bookmark_item.css_classes }}"{% endif %}>
<div class="content">
<div class="title">
<label class="form-checkbox bulk-edit-checkbox">
<input type="checkbox" name="bookmark_id" value="{{ bookmark_item.id }}">
<i class="form-icon"></i>
</label>
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
<img class="favicon" src="{% static bookmark_item.favicon_file %}" alt="">
{% endif %}
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener">
<span>{{ bookmark_item.title }}</span>
</a>
</div>
{% endif %}
{% if bookmark_list.description_display == 'inline' %}
<div class="description inline truncate">
{% if bookmark_item.tag_names %}
<span class="tags">
{% if bookmark_list.show_url %}
<div class="url-path truncate">
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
class="url-display">
{{ bookmark_item.url }}
</a>
</div>
{% endif %}
{% if bookmark_list.description_display == 'inline' %}
<div class="description inline truncate">
{% if bookmark_item.tag_names %}
<span class="tags">
{% for tag_name in bookmark_item.tag_names %}
<a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
{% endfor %}
</span>
{% endif %}
{% if bookmark_item.tag_names and bookmark_item.description %} | {% endif %}
{% endif %}
{% if bookmark_item.tag_names and bookmark_item.description %} | {% endif %}
{% if bookmark_item.description %}
<span>{{ bookmark_item.description }}</span>
{% endif %}
</div>
{% else %}
{% if bookmark_item.description %}
<span>{{ bookmark_item.description }}</span>
<div class="description separate">{{ bookmark_item.description }}</div>
{% endif %}
{% if bookmark_item.tag_names %}
<div class="tags">
{% for tag_name in bookmark_item.tag_names %}
<a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% else %}
{% if bookmark_item.description %}
<div class="description separate">{{ bookmark_item.description }}</div>
{% endif %}
{% if bookmark_item.tag_names %}
<div class="tags">
{% for tag_name in bookmark_item.tag_names %}
<a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
{% endfor %}
{% if bookmark_item.notes %}
<div class="notes">
<div class="markdown">{% markdown bookmark_item.notes %}</div>
</div>
{% endif %}
{% endif %}
{% if bookmark_item.notes %}
<div class="notes">
<div class="markdown">{% markdown bookmark_item.notes %}</div>
</div>
{% endif %}
<div class="actions">
{% if bookmark_item.display_date %}
{% if bookmark_item.web_archive_snapshot_url %}
<a href="{{ bookmark_item.web_archive_snapshot_url }}"
title="Show snapshot on the Internet Archive Wayback Machine"
target="{{ bookmark_list.link_target }}"
rel="noopener">
{{ bookmark_item.display_date }}
</a>
{% else %}
<span>{{ bookmark_item.display_date }}</span>
{% endif %}
<span>|</span>
{% endif %}
{# View link is visible for both owned and shared bookmarks #}
{% if bookmark_list.show_view_action %}
<a href="{{ bookmark_item.details_url }}" class="view-action"
data-turbo-action="replace" data-turbo-frame="details-modal">View</a>
{% endif %}
{% if bookmark_item.is_editable %}
{# Bookmark owner actions #}
{% if bookmark_list.show_edit_action %}
<a href="{% url 'bookmarks:edit' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}">Edit</a>
{% endif %}
{% if bookmark_list.show_archive_action %}
{% if bookmark_item.is_archived %}
<button type="submit" name="unarchive" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Unarchive
</button>
<div class="actions">
{% if bookmark_item.display_date %}
{% if bookmark_item.snapshot_url %}
<a href="{{ bookmark_item.snapshot_url }}"
title="{{ bookmark_item.snapshot_title }}"
target="{{ bookmark_list.link_target }}"
rel="noopener">
{{ bookmark_item.display_date }}
</a>
{% else %}
<button type="submit" name="archive" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Archive
<span>{{ bookmark_item.display_date }}</span>
{% endif %}
<span>|</span>
{% endif %}
{# View link is visible for both owned and shared bookmarks #}
{% if bookmark_list.show_view_action %}
<a href="{{ bookmark_item.details_url }}" class="view-action"
data-turbo-action="replace" data-turbo-frame="details-modal">View</a>
{% endif %}
{% if bookmark_item.is_editable %}
{# Bookmark owner actions #}
{% if bookmark_list.show_edit_action %}
<a href="{% url 'linkding:bookmarks.edit' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}">Edit</a>
{% endif %}
{% if bookmark_list.show_archive_action %}
{% if bookmark_item.is_archived %}
<button type="submit" name="unarchive" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Unarchive
</button>
{% else %}
<button type="submit" name="archive" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Archive
</button>
{% endif %}
{% endif %}
{% if bookmark_list.show_remove_action %}
<button ld-confirm-button type="submit" name="remove" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Remove
</button>
{% endif %}
{% endif %}
{% if bookmark_list.show_remove_action %}
<button ld-confirm-button type="submit" name="remove" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Remove
</button>
{% endif %}
{% else %}
{# Shared bookmark actions #}
<span>Shared by
{% else %}
{# Shared bookmark actions #}
<span>Shared by
<a href="?{% replace_query_param user=bookmark_item.owner.username %}">{{ bookmark_item.owner.username }}</a>
</span>
{% endif %}
{% if bookmark_item.has_extra_actions %}
<div class="extra-actions">
<span class="hide-sm">|</span>
{% if bookmark_item.show_mark_as_read %}
<button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm btn-icon"
ld-confirm-button ld-confirm-icon="ld-icon-read" ld-confirm-question="Mark as read?">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-unread"></use>
</svg>
Unread
</button>
{% endif %}
{% if bookmark_item.show_unshare %}
<button type="submit" name="unshare" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm btn-icon"
ld-confirm-button ld-confirm-icon="ld-icon-unshare" ld-confirm-question="Unshare?">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-share"></use>
</svg>
Shared
</button>
{% endif %}
{% if bookmark_item.show_notes_button %}
<button type="button" class="btn btn-link btn-sm btn-icon toggle-notes">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-note"></use>
</svg>
Notes
</button>
{% endif %}
{% endif %}
{% if bookmark_item.has_extra_actions %}
<div class="extra-actions">
<span class="hide-sm">|</span>
{% if bookmark_item.show_mark_as_read %}
<button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm btn-icon"
ld-confirm-button ld-confirm-icon="ld-icon-read" ld-confirm-question="Mark as read?">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-unread"></use>
</svg>
Unread
</button>
{% endif %}
{% if bookmark_item.show_unshare %}
<button type="submit" name="unshare" value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm btn-icon"
ld-confirm-button ld-confirm-icon="ld-icon-unshare" ld-confirm-question="Unshare?">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-share"></use>
</svg>
Shared
</button>
{% endif %}
{% if bookmark_item.show_notes_button %}
<button type="button" class="btn btn-link btn-sm btn-icon toggle-notes">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-note"></use>
</svg>
Notes
</button>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% if bookmark_list.show_preview_images %}
{% if bookmark_item.preview_image_file %}
<img class="preview-image" src="{% static bookmark_item.preview_image_file %}" loading="lazy"/>
{% else %}
<div class="preview-image placeholder">
<div class="img"/>
</div>
{% endif %}
</div>
</div>
{% if bookmark_list.show_preview_images %}
{% if bookmark_item.preview_image_file %}
<img class="preview-image" src="{% static bookmark_item.preview_image_file %}" loading="lazy"/>
{% else %}
<div class="preview-image placeholder">
<div class="img"/>
</div>
{% endif %}
{% endif %}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
<div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}">
{% pagination bookmark_list.bookmarks_page %}
</div>
<div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}">
{% pagination bookmark_list.bookmarks_page %}
</div>
</section>
{% endif %}

View File

@@ -22,6 +22,7 @@
<option value="bulk_share">Share</option>
<option value="bulk_unshare">Unshare</option>
{% endif %}
<option value="bulk_refresh">Refresh from website</option>
</select>
<div class="tag-autocomplete d-none" ld-tag-autocomplete>
<input name="bulk_tag_string" class="form-input input-sm" placeholder="Tag names..." variant="small">

View File

@@ -18,7 +18,7 @@
</div>
<div class="asset-actions">
{% if asset.file %}
<a class="btn btn-link" href="{% url 'bookmarks:assets.view' asset.id %}" target="_blank">View</a>
<a class="btn btn-link" href="{% url 'linkding:assets.view' asset.id %}" target="_blank">View</a>
{% endif %}
{% if details.is_editable %}
<button ld-confirm-button type="submit" name="remove_asset" value="{{ asset.id }}" class="btn btn-link">

View File

@@ -14,7 +14,7 @@
<span>{{ details.bookmark.url }}</span>
</a>
{% if details.latest_snapshot %}
<a class="weblink" href="{% url 'bookmarks:assets.read' details.latest_snapshot.id %}"
<a class="weblink" href="{% url 'linkding:assets.read' details.latest_snapshot.id %}"
target="{{ details.profile.bookmark_link_target }}">
{% if details.show_link_icons %}
<svg class="favicon" xmlns="http://www.w3.org/2000/svg">
@@ -85,7 +85,7 @@
<h3 id="details-modal-tags-title">Tags</h3>
<div>
{% for tag_name in details.bookmark.tag_names %}
<a href="{% url 'bookmarks:index' %}?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
<a href="{% url 'linkding:bookmarks.index' %}?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
{% endfor %}
</div>
</section>

View File

@@ -24,7 +24,7 @@
<div class="actions">
<div class="left-actions">
<a class="btn btn-wide"
href="{% url 'bookmarks:edit' details.bookmark.id %}?return_url={{ details.edit_return_url|urlencode }}">Edit</a>
href="{% url 'linkding:bookmarks.edit' details.bookmark.id %}?return_url={{ details.edit_return_url|urlencode }}">Edit</a>
</div>
<div class="right-actions">
<form action="{{ details.delete_url }}" method="post" data-turbo-action="replace">

View File

@@ -1,16 +1,21 @@
{% extends 'bookmarks/layout.html' %}
{% load bookmarks %}
{% block head %}
{% with page_title="Edit bookmark - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<div class="bookmarks-form-page">
<section class="content-area">
<div class="content-area-header">
<h2>Edit bookmark</h2>
<main aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">Edit bookmark</h1>
</div>
<form action="{% url 'bookmarks:edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post"
<form action="{% url 'linkding:bookmarks.edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post"
novalidate>
{% bookmark_form form return_url bookmark_id %}
{% include 'bookmarks/form.html' %}
</form>
</section>
</main>
</div>
{% endblock %}

View File

@@ -1,9 +1,9 @@
<div class="empty">
<p class="empty-title h5">You have no bookmarks yet</p>
<p class="empty-subtitle">
You can get started by <a href="{% url 'bookmarks:new' %}">adding</a> bookmarks,
<a href="{% url 'bookmarks:settings.general' %}">importing</a> your existing bookmarks or configuring the
<a href="{% url 'bookmarks:settings.integrations' %}">browser extension</a> or the <a
href="{% url 'bookmarks:settings.integrations' %}">bookmarklet</a>.
You can get started by <a href="{% url 'linkding:bookmarks.new' %}">adding</a> bookmarks,
<a href="{% url 'linkding:settings.general' %}">importing</a> your existing bookmarks or configuring the
<a href="{% url 'linkding:settings.integrations' %}">browser extension</a> or the <a
href="{% url 'linkding:settings.integrations' %}">bookmarklet</a>.
</p>
</div>

View File

@@ -33,9 +33,12 @@
<div class="form-group">
<div class="d-flex justify-between align-baseline">
<label for="{{ form.title.id_for_label }}" class="form-label">Title</label>
<button ld-clear-button data-for="{{ form.title.id_for_label }}" class="btn btn-link clear-button"
type="button">Clear
</button>
<div class="flex">
<button id="refresh-button" class="btn btn-link suffix-button" type="button">Refresh from website</button>
<button ld-clear-button data-for="{{ form.title.id_for_label }}" class="ml-2 btn btn-link suffix-button clear-button"
type="button">Clear
</button>
</div>
</div>
{{ form.title|add_class:"form-input"|attr:"autocomplete:off" }}
{{ form.title.errors }}
@@ -43,7 +46,8 @@
<div class="form-group">
<div class="d-flex justify-between align-baseline">
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
<button ld-clear-button data-for="{{ form.description.id_for_label }}" class="btn btn-link clear-button"
<button ld-clear-button data-for="{{ form.description.id_for_label }}"
class="btn btn-link suffix-button clear-button"
type="button">Clear
</button>
</div>
@@ -91,12 +95,12 @@
{% endif %}
<div class="divider"></div>
<div class="form-group d-flex justify-between">
{% if auto_close %}
{% if form.is_auto_close %}
<input type="submit" value="Save and close" class="btn btn-primary btn-wide">
{% else %}
<input type="submit" value="Save" class="btn btn-primary btn btn-primary btn-wide">
{% endif %}
<a href="{{ cancel_url }}" class="btn">Nevermind</a>
<a href="{{ return_url }}" class="btn">Nevermind</a>
</div>
<script type="application/javascript">
/**
@@ -111,8 +115,9 @@
const notesInput = document.getElementById('{{ form.notes.id_for_label }}');
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
const refreshButton = document.getElementById('refresh-button');
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
const editedBookmarkId = {{ bookmark_id }};
const editedBookmarkId = {{ form.instance.id|default:0 }};
let isTitleModified = !!titleInput.value;
let isDescriptionModified = !!descriptionInput.value;
@@ -144,7 +149,7 @@
toggleLoadingIcon(urlInput, true);
const websiteUrl = encodeURIComponent(urlInput.value);
const requestUrl = `{% url 'bookmarks:api-root' %}bookmarks/check?url=${websiteUrl}`;
const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}`;
fetch(requestUrl)
.then(response => response.json())
.then(data => {
@@ -154,6 +159,7 @@
// Display hint if URL is already bookmarked
const existingBookmark = data.bookmark;
bookmarkExistsHint.style['display'] = existingBookmark ? 'block' : 'none';
refreshButton.style['display'] = existingBookmark ? 'inline-block' : 'none';
// Prefill form with existing bookmark data
if (existingBookmark) {
@@ -193,6 +199,36 @@
});
}
function refreshMetadata() {
if (!urlInput.value) {
return;
}
toggleLoadingIcon(urlInput, true);
const websiteUrl = encodeURIComponent(urlInput.value);
const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}&ignore_cache=true`;
fetch(requestUrl)
.then(response => response.json())
.then(data => {
const metadata = data.metadata;
const existingBookmark = data.bookmark;
toggleLoadingIcon(urlInput, false);
if (metadata.title && metadata.title !== existingBookmark?.title) {
titleInput.value = metadata.title;
titleInput.classList.add("modified");
}
if (metadata.description && metadata.description !== existingBookmark?.description) {
descriptionInput.value = metadata.description;
descriptionInput.classList.add("modified");
}
});
}
refreshButton.addEventListener('click', refreshMetadata);
// Fetch website metadata when page loads and when URL changes, unless we are editing an existing bookmark
if (!editedBookmarkId) {
checkUrl();
@@ -203,6 +239,8 @@
descriptionInput.addEventListener('input', () => {
isDescriptionModified = true;
});
} else {
refreshButton.style['display'] = 'inline-block';
}
})();
</script>

View File

@@ -6,13 +6,14 @@
<link rel="icon" href="{% static 'favicon.svg' %}" sizes="any" type="image/svg+xml">
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'apple-touch-icon.png' %}">
<link rel="mask-icon" href="{% static 'safari-pinned-tab.svg' %}" color="#5856e0">
<link rel="manifest" href="{% url 'bookmarks:manifest' %}">
<link rel="manifest" href="{% url 'linkding:manifest' %}">
<link rel="search" type="application/opensearchdescription+xml" title="Linkding" href="{% url 'linkding:opensearch' %}"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="description" content="Self-hosted bookmark service">
<meta name="robots" content="index,follow">
<meta name="author" content="Sascha Ißbrücker">
<title>linkding</title>
<title>{{ page_title|default:'Linkding' }}</title>
{# Include specific theme variant based on user profile setting #}
{% if request.user_profile.theme == 'light' %}
<link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
@@ -30,7 +31,7 @@
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0">
{% endif %}
{% if request.user_profile.custom_css %}
<link href="{% url 'bookmarks:custom_css' %}?hash={{ request.user_profile.custom_css_hash }}" rel="stylesheet" type="text/css"/>
<link href="{% url 'linkding:custom_css' %}?hash={{ request.user_profile.custom_css_hash }}" rel="stylesheet" type="text/css"/>
{% endif %}
<meta name="turbo-cache-control" content="no-preview">
{% if not request.global_settings.enable_link_prefetch %}

View File

@@ -3,14 +3,16 @@
{% load shared %}
{% load bookmarks %}
{% block title %}Bookmarks - Linkding{% endblock %}
{% block content %}
<div ld-bulk-edit
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #}
<section class="main content-area col-2">
<div class="content-area-header mb-0">
<h2>Bookmarks</h2>
<main class="main col-2" aria-labelledby="main-heading">
<div class="section-header mb-0">
<h1 id="main-heading">Bookmarks</h1>
<div class="header-controls">
{% bookmark_search bookmark_list.search %}
{% include 'bookmarks/bulk_edit/toggle.html' %}
@@ -28,17 +30,19 @@
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form>
</section>
</main>
{# Tag cloud #}
<section class="side-panel content-area col-1">
<div class="content-area-header">
<h2>Tags</h2>
</div>
<div id="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
<div class="side-panel col-1">
<section aria-labelledby="tags-heading">
<div class="section-header">
<h2 id="tags-heading">Tags</h2>
</div>
<div id="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
</div>
</div>
{% endblock %}

View File

@@ -2,8 +2,8 @@
<!DOCTYPE html>
{# Use data attributes as storage for access in static scripts #}
<html lang="en" data-api-base-url="{% url 'bookmarks:api-root' %}">
{% include 'bookmarks/head.html' %}
<html lang="en" data-api-base-url="{% url 'linkding:api-root' %}">
{% block head %}{% include 'bookmarks/head.html' %}{% endblock %}
<body ld-global-shortcuts>
<div class="d-none">
@@ -68,7 +68,7 @@
<header class="container">
{% if has_toasts %}
<div class="toasts">
<form action="{% url 'bookmarks:toasts.acknowledge' %}?return_url={{ request.path | urlencode }}" method="post">
<form action="{% url 'linkding:toasts.acknowledge' %}?return_url={{ request.path | urlencode }}" method="post">
{% csrf_token %}
{% for toast in toast_messages %}
<div class="toast d-flex">
@@ -80,17 +80,19 @@
</div>
{% endif %}
<div class="d-flex justify-between">
<a href="{% url 'bookmarks:root' %}" class="d-flex align-center">
<img class="logo" src="{% static 'logo.png' %}" alt="Application logo">
<h1>LINKDING</h1>
<a href="{% url 'linkding:root' %}" class="app-link d-flex align-center">
<img class="app-logo" src="{% static 'logo.png' %}" alt="Application logo">
<span class="app-name">LINKDING</span>
</a>
{% if request.user.is_authenticated %}
{# Only show nav items menu when logged in #}
{% include 'bookmarks/nav_menu.html' %}
{% else %}
{# Otherwise show login link #}
<a href="{% url 'login' %}" class="btn btn-link">Login</a>
{% endif %}
<nav>
{% if request.user.is_authenticated %}
{# Only show nav items menu when logged in #}
{% include 'bookmarks/nav_menu.html' %}
{% else %}
{# Otherwise show login link #}
<a href="{% url 'login' %}" class="btn btn-link">Login</a>
{% endif %}
</nav>
</div>
</header>
<div class="content container">

View File

@@ -2,32 +2,49 @@
{% htmlmin %}
{# Basic menu list #}
<div class="hide-md">
<a href="{% url 'bookmarks:new' %}" class="btn btn-primary mr-2">Add bookmark</a>
<a href="{% url 'linkding:bookmarks.new' %}" class="btn btn-primary mr-2">Add bookmark</a>
<div ld-dropdown class="dropdown">
<button class="btn btn-link dropdown-toggle" tabindex="0">
Bookmarks
</button>
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'bookmarks:index' %}" class="menu-link">Active</a>
<a href="{% url 'linkding:bookmarks.index' %}" class="menu-link">Active</a>
</li>
<li class="menu-item">
<a href="{% url 'bookmarks:archived' %}" class="menu-link">Archived</a>
<a href="{% url 'linkding:bookmarks.archived' %}" class="menu-link">Archived</a>
</li>
{% if request.user_profile.enable_sharing %}
<li class="menu-item">
<a href="{% url 'bookmarks:shared' %}" class="menu-link">Shared</a>
<a href="{% url 'linkding:bookmarks.shared' %}" class="menu-link">Shared</a>
</li>
{% endif %}
<li class="menu-item">
<a href="{% url 'bookmarks:index' %}?unread=yes" class="menu-link">Unread</a>
<a href="{% url 'linkding:bookmarks.index' %}?unread=yes" class="menu-link">Unread</a>
</li>
<li class="menu-item">
<a href="{% url 'bookmarks:index' %}?q=!untagged" class="menu-link">Untagged</a>
<a href="{% url 'linkding:bookmarks.index' %}?q=!untagged" class="menu-link">Untagged</a>
</li>
</ul>
</div>
<a href="{% url 'bookmarks:settings.index' %}" class="btn btn-link">Settings</a>
<div ld-dropdown class="dropdown">
<button class="btn btn-link dropdown-toggle" tabindex="0">
Settings
</button>
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'linkding:settings.general' %}" class="menu-link">General</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:settings.integrations' %}" class="menu-link">Integrations</a>
</li>
{% if request.user.is_superuser %}
<li class="menu-item">
<a href="{% url 'admin:index' %}" class="menu-link" data-turbo="false">Admin</a>
</li>
{% endif %}
</ul>
</div>
<form class="d-inline" action="{% url 'logout' %}" method="post" data-turbo="false">
{% csrf_token %}
<button type="submit" class="btn btn-link">Logout</button>
@@ -35,7 +52,7 @@
</div>
{# Menu drop-down for smaller devices #}
<div class="show-md">
<a href="{% url 'bookmarks:new' %}" aria-label="Add bookmark" class="btn btn-primary">
<a href="{% url 'linkding:bookmarks.new' %}" aria-label="Add bookmark" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
style="width: 24px; height: 24px">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
@@ -51,26 +68,35 @@
<!-- menu component -->
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'bookmarks:index' %}" class="menu-link">Bookmarks</a>
<a href="{% url 'linkding:bookmarks.index' %}" class="menu-link">Bookmarks</a>
</li>
<li class="menu-item">
<a href="{% url 'bookmarks:archived' %}" class="menu-link">Archived</a>
<a href="{% url 'linkding:bookmarks.archived' %}" class="menu-link">Archived bookmarks</a>
</li>
{% if request.user_profile.enable_sharing %}
<li class="menu-item">
<a href="{% url 'bookmarks:shared' %}" class="menu-link">Shared</a>
<a href="{% url 'linkding:bookmarks.shared' %}" class="menu-link">Shared bookmarks</a>
</li>
{% endif %}
<li class="menu-item">
<a href="{% url 'bookmarks:index' %}?unread=yes" class="menu-link">Unread</a>
<a href="{% url 'linkding:bookmarks.index' %}?unread=yes" class="menu-link">Unread</a>
</li>
<li class="menu-item">
<a href="{% url 'bookmarks:index' %}?q=!untagged" class="menu-link">Untagged</a>
<a href="{% url 'linkding:bookmarks.index' %}?q=!untagged" class="menu-link">Untagged</a>
</li>
<div class="divider"></div>
<li class="menu-item">
<a href="{% url 'bookmarks:settings.index' %}" class="menu-link">Settings</a>
<a href="{% url 'linkding:settings.general' %}" class="menu-link">Settings</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:settings.integrations' %}" class="menu-link">Integrations</a>
</li>
{% if request.user.is_superuser %}
<li class="menu-item">
<a href="{% url 'admin:index' %}" class="menu-link" data-turbo="false">Admin</a>
</li>
{% endif %}
<div class="divider"></div>
<li class="menu-item">
<form class="d-inline" action="{% url 'logout' %}" method="post" data-turbo="false">
{% csrf_token %}

View File

@@ -1,15 +1,20 @@
{% extends 'bookmarks/layout.html' %}
{% load bookmarks %}
{% block head %}
{% with page_title="New bookmark - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<div class="bookmarks-form-page">
<section class="content-area">
<div class="content-area-header">
<h2>New bookmark</h2>
<main aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">New bookmark</h1>
</div>
<form action="{% url 'bookmarks:new' %}" method="post" novalidate>
{% bookmark_form form return_url auto_close=auto_close %}
<form action="{% url 'linkding:bookmarks.new' %}" method="post" novalidate>
{% include 'bookmarks/form.html' %}
</form>
</section>
</main>
</div>
{% endblock %}

View File

@@ -8,9 +8,9 @@
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #}
<section class="main content-area col-2">
<div class="content-area-header">
<h2>Shared bookmarks</h2>
<main class="main col-2" aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">Shared bookmarks</h1>
<div class="header-controls">
{% bookmark_search bookmark_list.search mode='shared' %}
<button ld-filter-drawer-trigger class="btn ml-2">Filters</button>
@@ -25,24 +25,28 @@
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form>
</section>
</main>
{# Filters #}
<section class="side-panel content-area col-1">
<div class="content-area-header">
<h2>User</h2>
</div>
<div>
{% user_select bookmark_list.search users %}
<br>
</div>
<div class="content-area-header">
<h2>Tags</h2>
</div>
<div id="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
<div class="side-panel col-1">
<section aria-labelledby="user-heading">
<div class="section-header">
<h2 id="user-heading">User</h2>
</div>
<div>
{% user_select bookmark_list.search users %}
<br>
</div>
</section>
<section aria-labelledby="tags-heading">
<div class="section-header">
<h2 id="tags-heading">Tags</h2>
</div>
<div id="tag-cloud-container">
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,19 @@
{% extends 'bookmarks/layout.html' %}
{% load widget_tweaks %}
{% block title %}Registration complete{% endblock %}
{% block head %}
{% with page_title="Registration complete - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<p>Registration complete. You can now use the application.</p>
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">Registration complete</h1>
</div>
<p class="text-success">
You can now use the application.
</p>
</main>
{% endblock %}

View File

@@ -1,12 +1,16 @@
{% extends 'bookmarks/layout.html' %}
{% load widget_tweaks %}
{% block title %}Registration{% endblock %}
{% block head %}
{% with page_title="Registration - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<section class="content-area mx-auto width-50 width-md-100">
<div class="content-area-header">
<h2>Register</h2>
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">Register</h1>
</div>
<form method="post" action="{% url 'django_registration_register' %}" novalidate>
{% csrf_token %}
@@ -34,5 +38,5 @@
<input type="submit" value="Register" class="btn btn-primary btn-wide">
<input type="hidden" name="next" value="{{ next }}">
</form>
</section>
</main>
{% endblock %}

View File

@@ -0,0 +1,7 @@
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>Linkding</ShortName>
<Description>Linkding</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">{{base_url}}static/favicon.ico</Image>
<Url type="text/html" template="{{ bookmarks_url }}?client=opensearch&amp;q={searchTerms}"/>
</OpenSearchDescription>

View File

@@ -1,12 +1,16 @@
{% extends 'bookmarks/layout.html' %}
{% load widget_tweaks %}
{% block title %}Login{% endblock %}
{% block head %}
{% with page_title="Login - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<section class="content-area mx-auto width-50 width-md-100">
<div class="content-area-header">
<h2>Login</h2>
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">Login</h1>
</div>
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
@@ -29,12 +33,12 @@
<input type="submit" value="Login" class="btn btn-primary btn-wide"/>
<input type="hidden" name="next" value="{{ next }}"/>
{% if enable_oidc %}
<a class="btn btn-link" href="{% url 'oidc_authentication_init' %}">Login with OIDC</a>
<a class="btn btn-link" href="{% url 'oidc_authentication_init' %}" data-turbo="false">Login with OIDC</a>
{% endif %}
{% if allow_registration %}
<a href="{% url 'django_registration_register' %}" class="btn btn-link">Register</a>
{% endif %}
</div>
</form>
</section>
</main>
{% endblock %}

View File

@@ -1,15 +1,19 @@
{% extends 'bookmarks/layout.html' %}
{% load widget_tweaks %}
{% block title %}Password changed{% endblock %}
{% block head %}
{% with page_title="Password changed - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<section class="content-area mx-auto width-50 width-md-100">
<div class="content-area-header">
<h2>Password Changed</h2>
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">Password Changed</h1>
</div>
<p class="text-success">
Your password was changed successfully.
</p>
</section>
</main>
{% endblock %}

View File

@@ -1,12 +1,16 @@
{% extends 'bookmarks/layout.html' %}
{% load widget_tweaks %}
{% block title %}Change Password{% endblock %}
{% block head %}
{% with page_title="Change password - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<section class="content-area mx-auto width-50 width-md-100">
<div class="content-area-header">
<h2>Change Password</h2>
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header">
<h1 id="main-heading">Change Password</h1>
</div>
<form method="post" action="{% url 'change_password' %}">
{% csrf_token %}
@@ -41,5 +45,5 @@
<br/>
<input type="submit" value="Change Password" class="btn btn-primary btn-wide">
</form>
</section>
</main>
{% endblock %}

View File

@@ -1,10 +1,15 @@
{% extends "bookmarks/layout.html" %}
{% load widget_tweaks %}
{% block content %}
<div class="settings-page">
{% block head %}
{% with page_title="Settings - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% include 'settings/nav.html' %}
{% block content %}
<main class="settings-page" aria-labelledby="main-heading">
<h1 id="main-heading">Settings</h1>
{# Profile section #}
{% if success_message %}
@@ -14,12 +19,12 @@
<div class="toast toast-error mb-4">{{ error_message }}</div>
{% endif %}
<section class="content-area">
<h2>Profile</h2>
<section aria-labelledby="profile-heading">
<h2 id="profile-heading">Profile</h2>
<p>
<a href="{% url 'change_password' %}">Change password</a>
</p>
<form action="{% url 'bookmarks:settings.update' %}" method="post" novalidate data-turbo="false">
<form action="{% url 'linkding:settings.update' %}" method="post" novalidate data-turbo="false">
{% csrf_token %}
<div class="form-group">
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
@@ -229,7 +234,7 @@ reddit.com/r/Music music reddit</pre>
<div class="form-input-hint">
Makes shared bookmarks publicly accessible, without requiring a login.
That means that anyone with a link to this instance can view shared bookmarks via the <a
href="{% url 'bookmarks:shared' %}">shared bookmarks page</a>.
href="{% url 'linkding:bookmarks.shared' %}">shared bookmarks page</a>.
</div>
</div>
{% if has_snapshot_support %}
@@ -278,9 +283,9 @@ reddit.com/r/Music music reddit</pre>
{# Global settings section #}
{% if global_settings_form %}
<section class="content-area">
<h2>Global settings</h2>
<form action="{% url 'bookmarks:settings.update' %}" method="post" novalidate data-turbo="false">
<section aria-labelledby="global-settings-heading">
<h2 id="global-settings-heading">Global settings</h2>
<form action="{% url 'linkding:settings.update' %}" method="post" novalidate data-turbo="false">
{% csrf_token %}
<div class="form-group">
<label for="{{ global_settings_form.landing_page.id_for_label }}" class="form-label">Landing page</label>
@@ -318,11 +323,11 @@ reddit.com/r/Music music reddit</pre>
{% endif %}
{# Import section #}
<section class="content-area">
<h2>Import</h2>
<section aria-labelledby="import-heading">
<h2 id="import-heading">Import</h2>
<p>Import bookmarks and tags in the Netscape HTML format. This will execute a sync where new bookmarks are
added and existing ones are updated.</p>
<form method="post" enctype="multipart/form-data" action="{% url 'bookmarks:settings.import' %}">
<form method="post" enctype="multipart/form-data" action="{% url 'linkding:settings.import' %}">
{% csrf_token %}
<div class="form-group">
<label for="import_map_private_flag" class="form-checkbox">
@@ -346,10 +351,10 @@ reddit.com/r/Music music reddit</pre>
</section>
{# Export section #}
<section class="content-area">
<h2>Export</h2>
<section aria-labelledby="export-heading">
<h2 id="export-heading">Export</h2>
<p>Export all bookmarks in Netscape HTML format.</p>
<a class="btn btn-primary" target="_blank" href="{% url 'bookmarks:settings.export' %}">Download (.html)</a>
<a class="btn btn-primary" target="_blank" href="{% url 'linkding:settings.export' %}">Download (.html)</a>
{% if export_error %}
<div class="has-error">
<p class="form-input-hint">
@@ -360,8 +365,8 @@ reddit.com/r/Music music reddit</pre>
</section>
{# About section #}
<section class="content-area about">
<h2>About</h2>
<section class="about" aria-labelledby="about-heading">
<h2 id="about-heading">About</h2>
<table class="table">
<tbody>
<tr>
@@ -384,7 +389,7 @@ reddit.com/r/Music music reddit</pre>
</tbody>
</table>
</section>
</div>
</main>
<script>
(function init() {

View File

@@ -1,12 +1,17 @@
{% extends "bookmarks/layout.html" %}
{% block head %}
{% with page_title="Integrations - Linkding" %}
{{ block.super }}
{% endwith %}
{% endblock %}
{% block content %}
<div class="settings-page">
<main class="settings-page" aria-labelledby="main-heading">
<h1 id="main-heading">Integrations</h1>
{% include 'settings/nav.html' %}
<section class="content-area">
<h2>Browser Extension</h2>
<section aria-labelledby="browser-extension-heading">
<h2 id="browser-extension-heading">Browser Extension</h2>
<p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The
extension is available in the official extension stores for:</p>
<ul>
@@ -31,8 +36,8 @@
class="btn btn-primary">📎 Add bookmark</a>
</section>
<section class="content-area">
<h2>REST API</h2>
<section aria-labelledby="rest-api-heading">
<h2 id="rest-api-heading">REST API</h2>
<p>The following token can be used to authenticate 3rd-party applications against the REST API:</p>
<div class="form-group">
<div class="width-50 width-md-100">
@@ -48,8 +53,8 @@
</p>
</section>
<section class="content-area">
<h2>RSS Feeds</h2>
<section aria-labelledby="rss-feeds-heading">
<h2 id="rss-feeds-heading">RSS Feeds</h2>
<p>The following URLs provide RSS feeds for your bookmarks:</p>
<ul style="list-style-position: outside;">
<li><a target="_blank" href="{{ all_feed_url }}">All bookmarks</a></li>
@@ -84,5 +89,5 @@
After deleting the feed token, new URLs will be generated when you reload this settings page.
</p>
</section>
</div>
</main>
{% endblock %}

View File

@@ -1,24 +0,0 @@
{% url 'bookmarks:settings.index' as index_url %}
{% url 'bookmarks:settings.general' as general_url %}
{% url 'bookmarks:settings.integrations' as integrations_url %}
<ul class="tab tab-block">
<li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == general_url%}active{% endif %}">
<a href="{% url 'bookmarks:settings.general' %}">General</a>
</li>
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
</li>
{% if request.user.is_superuser %}
<li class="tab-item">
<a href="{% url 'admin:index' %}" target="_blank">
<span>Admin</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="ml-1" style="width: 1.2em; height: 1.2em; vertical-align: -0.2em;">
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
</a>
</li>
{% endif %}
</ul>
<br>

View File

@@ -3,7 +3,6 @@ from typing import List
from django import template
from bookmarks.models import (
BookmarkForm,
BookmarkSearch,
BookmarkSearchForm,
User,
@@ -12,23 +11,6 @@ from bookmarks.models import (
register = template.Library()
@register.inclusion_tag("bookmarks/form.html", name="bookmark_form", takes_context=True)
def bookmark_form(
context,
form: BookmarkForm,
cancel_url: str,
bookmark_id: int = 0,
auto_close: bool = False,
):
return {
"request": context["request"],
"form": form,
"auto_close": auto_close,
"bookmark_id": bookmark_id,
"cancel_url": cancel_url,
}
@register.inclusion_tag(
"bookmarks/search.html", name="bookmark_search", takes_context=True
)

View File

@@ -142,5 +142,6 @@ def render_markdown(context, markdown_text):
as_html = renderer.convert(markdown_text)
sanitized_html = bleach.clean(as_html, markdown_tags, markdown_attrs)
linkified_html = bleach.linkify(sanitized_html)
return mark_safe(sanitized_html)
return mark_safe(linkified_html)

View File

@@ -10,7 +10,6 @@ from unittest import TestCase
from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib.auth.models import User
from django.test import override_settings
from django.utils import timezone
from django.utils.crypto import get_random_string
@@ -18,7 +17,7 @@ from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase
from bookmarks.models import Bookmark, BookmarkAsset, Tag
from bookmarks.models import Bookmark, BookmarkAsset, Tag, User
class BookmarkFactoryMixin:

View File

@@ -7,7 +7,7 @@ from django.test import TestCase
class AppOptionsTestCase(TestCase):
def setUp(self) -> None:
self.settings_module = importlib.import_module("siteroot.settings.base")
self.settings_module = importlib.import_module("bookmarks.settings.base")
def test_empty_csrf_trusted_origins(self):
module = importlib.reload(self.settings_module)

View File

@@ -1,6 +1,7 @@
import datetime
import gzip
import os
from datetime import timedelta
from unittest import mock
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -236,3 +237,175 @@ class AssetServiceTestCase(TestCase, BookmarkFactoryMixin):
# asset is not saved to the database
self.assertIsNone(BookmarkAsset.objects.first())
def test_create_snapshot_updates_bookmark_latest_snapshot(self):
bookmark = self.setup_bookmark(url="https://example.com")
first_asset = assets.create_snapshot_asset(bookmark)
first_asset.save()
assets.create_snapshot(first_asset)
bookmark.refresh_from_db()
self.assertEqual(bookmark.latest_snapshot, first_asset)
second_asset = assets.create_snapshot_asset(bookmark)
second_asset.save()
assets.create_snapshot(second_asset)
bookmark.refresh_from_db()
self.assertEqual(bookmark.latest_snapshot, second_asset)
def test_upload_snapshot_updates_bookmark_latest_snapshot(self):
bookmark = self.setup_bookmark(url="https://example.com")
first_asset = assets.upload_snapshot(bookmark, self.html_content.encode())
bookmark.refresh_from_db()
self.assertEqual(bookmark.latest_snapshot, first_asset)
second_asset = assets.upload_snapshot(bookmark, self.html_content.encode())
bookmark.refresh_from_db()
self.assertEqual(bookmark.latest_snapshot, second_asset)
self.assertNotEqual(bookmark.latest_snapshot, first_asset)
def test_create_snapshot_failure_does_not_update_latest_snapshot(self):
# Create a bookmark with an existing latest_snapshot
bookmark = self.setup_bookmark(url="https://example.com")
initial_snapshot = assets.upload_snapshot(bookmark, self.html_content.encode())
bookmark.refresh_from_db()
self.assertEqual(bookmark.latest_snapshot, initial_snapshot)
# Create a new snapshot asset that will fail
failing_asset = assets.create_snapshot_asset(bookmark)
failing_asset.save()
# Make the snapshot creation fail
self.mock_singlefile_create_snapshot.side_effect = Exception(
"Snapshot creation failed"
)
# Attempt to create a snapshot (which will fail)
with self.assertRaises(Exception):
assets.create_snapshot(failing_asset)
# Verify that the bookmark's latest_snapshot is still the initial snapshot
bookmark.refresh_from_db()
self.assertEqual(bookmark.latest_snapshot, initial_snapshot)
def test_upload_snapshot_failure_does_not_update_latest_snapshot(self):
# Create a bookmark with an existing latest_snapshot
bookmark = self.setup_bookmark(url="https://example.com")
initial_snapshot = assets.upload_snapshot(bookmark, self.html_content.encode())
bookmark.refresh_from_db()
self.assertEqual(bookmark.latest_snapshot, initial_snapshot)
# Make the gzip.open function fail
with mock.patch("gzip.open") as mock_gzip_open:
mock_gzip_open.side_effect = Exception("Upload failed")
# Attempt to upload a snapshot (which will fail)
with self.assertRaises(Exception):
assets.upload_snapshot(bookmark, b"New content")
# Verify that the bookmark's latest_snapshot is still the initial snapshot
bookmark.refresh_from_db()
self.assertEqual(bookmark.latest_snapshot, initial_snapshot)
def test_remove_latest_snapshot_updates_bookmark(self):
# Create a bookmark with multiple snapshots
bookmark = self.setup_bookmark()
# Create base time (1 hour ago)
base_time = timezone.now() - timedelta(hours=1)
# Create three snapshots with explicitly different dates
old_asset = self.setup_asset(
bookmark=bookmark,
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
status=BookmarkAsset.STATUS_COMPLETE,
file="old_snapshot.html.gz",
date_created=base_time,
)
self.setup_asset_file(old_asset)
middle_asset = self.setup_asset(
bookmark=bookmark,
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
status=BookmarkAsset.STATUS_COMPLETE,
file="middle_snapshot.html.gz",
date_created=base_time + timedelta(minutes=30),
)
self.setup_asset_file(middle_asset)
latest_asset = self.setup_asset(
bookmark=bookmark,
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
status=BookmarkAsset.STATUS_COMPLETE,
file="latest_snapshot.html.gz",
date_created=base_time + timedelta(minutes=60),
)
self.setup_asset_file(latest_asset)
# Set the latest asset as the bookmark's latest_snapshot
bookmark.latest_snapshot = latest_asset
bookmark.save()
# Delete the latest snapshot
assets.remove_asset(latest_asset)
bookmark.refresh_from_db()
# Verify that middle_asset is now the latest_snapshot
self.assertEqual(bookmark.latest_snapshot, middle_asset)
# Delete the middle snapshot
assets.remove_asset(middle_asset)
bookmark.refresh_from_db()
# Verify that old_asset is now the latest_snapshot
self.assertEqual(bookmark.latest_snapshot, old_asset)
# Delete the last snapshot
assets.remove_asset(old_asset)
bookmark.refresh_from_db()
# Verify that latest_snapshot is now None
self.assertIsNone(bookmark.latest_snapshot)
def test_remove_non_latest_snapshot_does_not_affect_bookmark(self):
# Create a bookmark with multiple snapshots
bookmark = self.setup_bookmark()
# Create base time (1 hour ago)
base_time = timezone.now() - timedelta(hours=1)
# Create two snapshots with explicitly different dates
old_asset = self.setup_asset(
bookmark=bookmark,
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
status=BookmarkAsset.STATUS_COMPLETE,
file="old_snapshot.html.gz",
date_created=base_time,
)
self.setup_asset_file(old_asset)
latest_asset = self.setup_asset(
bookmark=bookmark,
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
status=BookmarkAsset.STATUS_COMPLETE,
file="latest_snapshot.html.gz",
date_created=base_time + timedelta(minutes=30),
)
self.setup_asset_file(latest_asset)
# Set the latest asset as the bookmark's latest_snapshot
bookmark.latest_snapshot = latest_asset
bookmark.save()
# Delete the old snapshot (not the latest)
assets.remove_asset(old_asset)
bookmark.refresh_from_db()
# Verify that latest_snapshot hasn't changed
self.assertEqual(bookmark.latest_snapshot, latest_asset)

View File

@@ -16,17 +16,17 @@ class AuthApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def test_auth_with_token_keyword(self):
self.authenticate("Token")
url = reverse("bookmarks:user-profile")
url = reverse("linkding:user-profile")
self.get(url, expected_status_code=status.HTTP_200_OK)
def test_auth_with_bearer_keyword(self):
self.authenticate("Bearer")
url = reverse("bookmarks:user-profile")
url = reverse("linkding:user-profile")
self.get(url, expected_status_code=status.HTTP_200_OK)
def test_auth_with_unknown_keyword(self):
self.authenticate("Key")
url = reverse("bookmarks:user-profile")
url = reverse("linkding:user-profile")
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)

View File

@@ -21,7 +21,7 @@ class AuthProxySupportTest(TestCase):
)
headers = {"REMOTE_USER": user.username}
response = self.client.get(reverse("bookmarks:index"), **headers)
response = self.client.get(reverse("linkding:bookmarks.index"), **headers)
self.assertEqual(response.status_code, 200)
@@ -43,7 +43,7 @@ class AuthProxySupportTest(TestCase):
)
headers = {"Custom-User": user.username}
response = self.client.get(reverse("bookmarks:index"), **headers)
response = self.client.get(reverse("linkding:bookmarks.index"), **headers)
self.assertEqual(response.status_code, 200)
@@ -53,6 +53,8 @@ class AuthProxySupportTest(TestCase):
)
headers = {"REMOTE_USER": user.username}
response = self.client.get(reverse("bookmarks:index"), **headers, follow=True)
response = self.client.get(
reverse("linkding:bookmarks.index"), **headers, follow=True
)
self.assertRedirects(response, "/login/?next=%2Fbookmarks")

View File

@@ -202,3 +202,44 @@ class AutoTaggingTestCase(TestCase):
tags = auto_tagging.get_tags(script, url)
self.assertEqual(tags, {"tag1", "tag2"})
def test_auto_tag_with_url_fragment(self):
script = """
example.com/#/section/1 section1
example.com/#/section/2 section2
"""
url = "https://example.com/#/section/1"
tags = auto_tagging.get_tags(script, url)
self.assertEqual(tags, {"section1"})
def test_auto_tag_with_url_fragment_partial_match(self):
script = """
example.com/#/section section
"""
url = "https://example.com/#/section/1"
tags = auto_tagging.get_tags(script, url)
self.assertEqual(tags, {"section"})
def test_auto_tag_with_url_fragment_ignores_case(self):
script = """
example.com/#SECTION section
"""
url = "https://example.com/#section"
tags = auto_tagging.get_tags(script, url)
self.assertEqual(tags, {"section"})
def test_auto_tag_with_url_fragment_and_comment(self):
script = """
example.com/#section1 section1 #This is a comment
"""
url = "https://example.com/#section1"
tags = auto_tagging.get_tags(script, url)
self.assertEqual(tags, {"section1"})

View File

@@ -37,7 +37,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark()
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"archive": [bookmark.id],
},
@@ -54,7 +54,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(user=other_user)
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"archive": [bookmark.id],
},
@@ -69,7 +69,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(is_archived=True)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"unarchive": [bookmark.id],
},
@@ -85,7 +85,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(is_archived=True, user=other_user)
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"unarchive": [bookmark.id],
},
@@ -99,7 +99,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark()
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"remove": [bookmark.id],
},
@@ -114,7 +114,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(user=other_user)
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"remove": [bookmark.id],
},
@@ -126,7 +126,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(unread=True)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"mark_as_read": [bookmark.id],
},
@@ -139,7 +139,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(shared=True)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"unshare": [bookmark.id],
},
@@ -156,7 +156,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(user=other_user, shared=True)
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"unshare": [bookmark.id],
},
@@ -172,7 +172,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark()
with patch.object(tasks, "_create_html_snapshot_task"):
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"create_html_snapshot": [bookmark.id],
},
@@ -187,7 +187,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(user=other_user)
with patch.object(tasks, "_create_html_snapshot_task"):
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"create_html_snapshot": [bookmark.id],
},
@@ -202,7 +202,7 @@ class BookmarkActionViewTestCase(
with patch.object(assets, "upload_asset") as mock_upload_asset:
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{"upload_asset": bookmark.id, "upload_asset_file": upload_file},
)
self.assertEqual(response.status_code, 302)
@@ -223,7 +223,7 @@ class BookmarkActionViewTestCase(
with patch.object(assets, "upload_asset") as mock_upload_asset:
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{"upload_asset": bookmark.id, "upload_asset_file": upload_file},
)
self.assertEqual(response.status_code, 404)
@@ -237,7 +237,7 @@ class BookmarkActionViewTestCase(
upload_file = SimpleUploadedFile("test.txt", file_content)
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{"upload_asset": bookmark.id, "upload_asset_file": upload_file},
)
self.assertEqual(response.status_code, 403)
@@ -246,7 +246,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark()
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{"upload_asset": bookmark.id},
)
self.assertEqual(response.status_code, 400)
@@ -256,7 +256,7 @@ class BookmarkActionViewTestCase(
asset = self.setup_asset(bookmark)
response = self.client.post(
reverse("bookmarks:index.action"), {"remove_asset": asset.id}
reverse("linkding:bookmarks.index.action"), {"remove_asset": asset.id}
)
self.assertEqual(response.status_code, 302)
self.assertFalse(BookmarkAsset.objects.filter(id=asset.id).exists())
@@ -267,7 +267,7 @@ class BookmarkActionViewTestCase(
asset = self.setup_asset(bookmark)
response = self.client.post(
reverse("bookmarks:index.action"), {"remove_asset": asset.id}
reverse("linkding:bookmarks.index.action"), {"remove_asset": asset.id}
)
self.assertEqual(response.status_code, 404)
self.assertTrue(BookmarkAsset.objects.filter(id=asset.id).exists())
@@ -276,7 +276,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark()
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"update_state": bookmark.id,
"is_archived": "on",
@@ -296,7 +296,7 @@ class BookmarkActionViewTestCase(
bookmark = self.setup_bookmark(user=other_user)
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"update_state": bookmark.id,
"is_archived": "on",
@@ -317,7 +317,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark()
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_archive"],
"bulk_execute": [""],
@@ -342,7 +342,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(user=other_user)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_archive"],
"bulk_execute": [""],
@@ -364,7 +364,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(is_archived=True)
self.client.post(
reverse("bookmarks:archived.action"),
reverse("linkding:bookmarks.archived.action"),
{
"bulk_action": ["bulk_unarchive"],
"bulk_execute": [""],
@@ -389,7 +389,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(is_archived=True, user=other_user)
self.client.post(
reverse("bookmarks:archived.action"),
reverse("linkding:bookmarks.archived.action"),
{
"bulk_action": ["bulk_unarchive"],
"bulk_execute": [""],
@@ -411,7 +411,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark()
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_delete"],
"bulk_execute": [""],
@@ -436,7 +436,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(user=other_user)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_delete"],
"bulk_execute": [""],
@@ -460,7 +460,7 @@ class BookmarkActionViewTestCase(
tag2 = self.setup_tag()
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_tag"],
"bulk_execute": [""],
@@ -492,7 +492,7 @@ class BookmarkActionViewTestCase(
tag2 = self.setup_tag()
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_tag"],
"bulk_execute": [""],
@@ -521,7 +521,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_untag"],
"bulk_execute": [""],
@@ -553,7 +553,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_untag"],
"bulk_execute": [""],
@@ -580,7 +580,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(unread=True)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_read"],
"bulk_execute": [""],
@@ -605,7 +605,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(unread=True, user=other_user)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_read"],
"bulk_execute": [""],
@@ -627,7 +627,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(unread=False)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_unread"],
"bulk_execute": [""],
@@ -652,7 +652,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(unread=False, user=other_user)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_unread"],
"bulk_execute": [""],
@@ -674,7 +674,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(shared=False)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_share"],
"bulk_execute": [""],
@@ -699,7 +699,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(shared=False, user=other_user)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_share"],
"bulk_execute": [""],
@@ -721,7 +721,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(shared=True)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_unshare"],
"bulk_execute": [""],
@@ -746,7 +746,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark(shared=True, user=other_user)
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_unshare"],
"bulk_execute": [""],
@@ -768,7 +768,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark()
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_archive"],
"bulk_execute": [""],
@@ -784,7 +784,7 @@ class BookmarkActionViewTestCase(
self.setup_numbered_bookmarks(100)
self.client.post(
reverse("bookmarks:index.action") + "?page=2",
reverse("linkding:bookmarks.index.action") + "?page=2",
{
"bulk_action": ["bulk_delete"],
"bulk_execute": [""],
@@ -813,7 +813,7 @@ class BookmarkActionViewTestCase(
self.assertIsNotNone(Bookmark.objects.filter(title="Bookmark 3").first())
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_delete"],
"bulk_execute": [""],
@@ -833,7 +833,7 @@ class BookmarkActionViewTestCase(
self.assertEqual(3, Bookmark.objects.filter(title__startswith="foo").count())
self.client.post(
reverse("bookmarks:index.action") + "?q=foo",
reverse("linkding:bookmarks.index.action") + "?q=foo",
{
"bulk_action": ["bulk_delete"],
"bulk_execute": [""],
@@ -858,7 +858,7 @@ class BookmarkActionViewTestCase(
)
self.client.post(
reverse("bookmarks:archived.action"),
reverse("linkding:bookmarks.archived.action"),
{
"bulk_action": ["bulk_delete"],
"bulk_execute": [""],
@@ -878,7 +878,7 @@ class BookmarkActionViewTestCase(
self.assertEqual(3, Bookmark.objects.filter(title__startswith="foo").count())
self.client.post(
reverse("bookmarks:archived.action") + "?q=foo",
reverse("linkding:bookmarks.archived.action") + "?q=foo",
{
"bulk_action": ["bulk_delete"],
"bulk_execute": [""],
@@ -893,7 +893,7 @@ class BookmarkActionViewTestCase(
self.setup_bulk_edit_scope_test_data()
response = self.client.post(
reverse("bookmarks:shared.action"),
reverse("linkding:bookmarks.shared.action"),
{
"bulk_action": ["bulk_delete"],
"bulk_execute": [""],
@@ -908,7 +908,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark()
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_archive"],
"bulk_execute": [""],
@@ -917,7 +917,7 @@ class BookmarkActionViewTestCase(
self.assertEqual(response.status_code, 302)
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bulk_action": ["bulk_archive"],
"bulk_execute": [""],
@@ -934,7 +934,7 @@ class BookmarkActionViewTestCase(
bookmark3 = self.setup_bookmark()
self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
{
"bookmark_id": [
str(bookmark1.id),
@@ -947,22 +947,22 @@ class BookmarkActionViewTestCase(
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
def test_index_action_redirects_to_index_with_query_params(self):
url = reverse("bookmarks:index.action") + "?q=foo&page=2"
redirect_url = reverse("bookmarks:index") + "?q=foo&page=2"
url = reverse("linkding:bookmarks.index.action") + "?q=foo&page=2"
redirect_url = reverse("linkding:bookmarks.index") + "?q=foo&page=2"
response = self.client.post(url)
self.assertRedirects(response, redirect_url)
def test_archived_action_redirects_to_archived_with_query_params(self):
url = reverse("bookmarks:archived.action") + "?q=foo&page=2"
redirect_url = reverse("bookmarks:archived") + "?q=foo&page=2"
url = reverse("linkding:bookmarks.archived.action") + "?q=foo&page=2"
redirect_url = reverse("linkding:bookmarks.archived") + "?q=foo&page=2"
response = self.client.post(url)
self.assertRedirects(response, redirect_url)
def test_shared_action_redirects_to_shared_with_query_params(self):
url = reverse("bookmarks:shared.action") + "?q=foo&page=2"
redirect_url = reverse("bookmarks:shared") + "?q=foo&page=2"
url = reverse("linkding:bookmarks.shared.action") + "?q=foo&page=2"
redirect_url = reverse("linkding:bookmarks.shared") + "?q=foo&page=2"
response = self.client.post(url)
self.assertRedirects(response, redirect_url)
@@ -1012,7 +1012,7 @@ class BookmarkActionViewTestCase(
def test_index_action_with_turbo_returns_bookmark_update(self):
fixture = self.bookmark_update_fixture()
response = self.client.post(
reverse("bookmarks:index.action"),
reverse("linkding:bookmarks.index.action"),
HTTP_ACCEPT="text/vnd.turbo-stream.html",
)
@@ -1030,7 +1030,7 @@ class BookmarkActionViewTestCase(
def test_archived_action_with_turbo_returns_bookmark_update(self):
fixture = self.bookmark_update_fixture()
response = self.client.post(
reverse("bookmarks:archived.action"),
reverse("linkding:bookmarks.archived.action"),
HTTP_ACCEPT="text/vnd.turbo-stream.html",
)
@@ -1048,7 +1048,7 @@ class BookmarkActionViewTestCase(
def test_shared_action_with_turbo_returns_bookmark_update(self):
fixture = self.bookmark_update_fixture()
response = self.client.post(
reverse("bookmarks:shared.action"),
reverse("linkding:bookmarks.shared.action"),
HTTP_ACCEPT="text/vnd.turbo-stream.html",
)

View File

@@ -46,7 +46,7 @@ class BookmarkArchivedViewTestCase(
self.setup_bookmark(is_archived=True, user=other_user),
]
response = self.client.get(reverse("bookmarks:archived"))
response = self.client.get(reverse("linkding:bookmarks.archived"))
self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks)
@@ -59,7 +59,7 @@ class BookmarkArchivedViewTestCase(
3, prefix="bar", archived=True
)
response = self.client.get(reverse("bookmarks:archived") + "?q=foo")
response = self.client.get(reverse("linkding:bookmarks.archived") + "?q=foo")
html = collapse_whitespace(response.content.decode())
self.assertVisibleBookmarks(response, visible_bookmarks)
@@ -84,7 +84,7 @@ class BookmarkArchivedViewTestCase(
unarchived_bookmarks + other_user_bookmarks
)
response = self.client.get(reverse("bookmarks:archived"))
response = self.client.get(reverse("linkding:bookmarks.archived"))
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
@@ -100,7 +100,7 @@ class BookmarkArchivedViewTestCase(
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks)
response = self.client.get(reverse("bookmarks:archived") + "?q=foo")
response = self.client.get(reverse("linkding:bookmarks.archived") + "?q=foo")
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
@@ -132,7 +132,7 @@ class BookmarkArchivedViewTestCase(
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
read_tags = self.get_tags_from_bookmarks(read_bookmarks)
response = self.client.get(reverse("bookmarks:archived"))
response = self.client.get(reverse("linkding:bookmarks.archived"))
self.assertVisibleBookmarks(response, unread_bookmarks)
self.assertInvisibleBookmarks(response, read_bookmarks)
self.assertVisibleTags(response, unread_tags)
@@ -149,7 +149,8 @@ class BookmarkArchivedViewTestCase(
self.setup_bookmark(is_archived=True, tags=tags)
response = self.client.get(
reverse("bookmarks:archived") + f"?q=%23{tags[0].name}+%23{tags[1].name}"
reverse("linkding:bookmarks.archived")
+ f"?q=%23{tags[0].name}+%23{tags[1].name}"
)
self.assertSelectedTags(response, [tags[0], tags[1]])
@@ -167,7 +168,7 @@ class BookmarkArchivedViewTestCase(
self.setup_bookmark(title=tags[0].name, tags=tags, is_archived=True)
response = self.client.get(
reverse("bookmarks:archived")
reverse("linkding:bookmarks.archived")
+ f"?q={tags[0].name}+%23{tags[1].name.upper()}"
)
@@ -187,7 +188,7 @@ class BookmarkArchivedViewTestCase(
self.setup_bookmark(tags=tags, is_archived=True)
response = self.client.get(
reverse("bookmarks:archived")
reverse("linkding:bookmarks.archived")
+ f"?q={tags[0].name}+%23{tags[1].name.upper()}"
)
@@ -196,7 +197,7 @@ class BookmarkArchivedViewTestCase(
def test_should_open_bookmarks_in_new_page_by_default(self):
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
response = self.client.get(reverse("bookmarks:archived"))
response = self.client.get(reverse("linkding:bookmarks.archived"))
self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
@@ -207,14 +208,14 @@ class BookmarkArchivedViewTestCase(
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
response = self.client.get(reverse("bookmarks:archived"))
response = self.client.get(reverse("linkding:bookmarks.archived"))
self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
def test_edit_link_return_url_respects_search_options(self):
bookmark = self.setup_bookmark(title="foo", is_archived=True)
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
base_url = reverse("bookmarks:archived")
edit_url = reverse("linkding:bookmarks.edit", args=[bookmark.id])
base_url = reverse("linkding:bookmarks.archived")
# without query params
return_url = urllib.parse.quote(base_url)
@@ -240,8 +241,8 @@ class BookmarkArchivedViewTestCase(
self.assertEditLink(response, url)
def test_bulk_edit_respects_search_options(self):
action_url = reverse("bookmarks:archived.action")
base_url = reverse("bookmarks:archived")
action_url = reverse("linkding:bookmarks.archived.action")
base_url = reverse("linkding:bookmarks.archived")
# without params
url = f"{action_url}"
@@ -264,7 +265,7 @@ class BookmarkArchivedViewTestCase(
self.assertBulkActionForm(response, url)
def test_allowed_bulk_actions(self):
url = reverse("bookmarks:archived")
url = reverse("linkding:bookmarks.archived")
response = self.client.get(url)
html = response.content.decode()
@@ -277,6 +278,7 @@ class BookmarkArchivedViewTestCase(
<option value="bulk_untag">Remove tags</option>
<option value="bulk_read">Mark as read</option>
<option value="bulk_unread">Mark as unread</option>
<option value="bulk_refresh">Refresh from website</option>
</select>
""",
html,
@@ -287,7 +289,7 @@ class BookmarkArchivedViewTestCase(
user_profile.enable_sharing = True
user_profile.save()
url = reverse("bookmarks:archived")
url = reverse("linkding:bookmarks.archived")
response = self.client.get(url)
html = response.content.decode()
@@ -302,6 +304,7 @@ class BookmarkArchivedViewTestCase(
<option value="bulk_unread">Mark as unread</option>
<option value="bulk_share">Share</option>
<option value="bulk_unshare">Unshare</option>
<option value="bulk_refresh">Refresh from website</option>
</select>
""",
html,
@@ -309,13 +312,13 @@ class BookmarkArchivedViewTestCase(
def test_apply_search_preferences(self):
# no params
response = self.client.post(reverse("bookmarks:archived"))
response = self.client.post(reverse("linkding:bookmarks.archived"))
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("bookmarks:archived"))
self.assertEqual(response.url, reverse("linkding:bookmarks.archived"))
# some params
response = self.client.post(
reverse("bookmarks:archived"),
reverse("linkding:bookmarks.archived"),
{
"q": "foo",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -323,12 +326,13 @@ class BookmarkArchivedViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, reverse("bookmarks:archived") + "?q=foo&sort=title_asc"
response.url,
reverse("linkding:bookmarks.archived") + "?q=foo&sort=title_asc",
)
# params with default value are removed
response = self.client.post(
reverse("bookmarks:archived"),
reverse("linkding:bookmarks.archived"),
{
"q": "foo",
"user": "",
@@ -339,12 +343,12 @@ class BookmarkArchivedViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, reverse("bookmarks:archived") + "?q=foo&unread=yes"
response.url, reverse("linkding:bookmarks.archived") + "?q=foo&unread=yes"
)
# page is removed
response = self.client.post(
reverse("bookmarks:archived"),
reverse("linkding:bookmarks.archived"),
{
"q": "foo",
"page": "2",
@@ -353,7 +357,8 @@ class BookmarkArchivedViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, reverse("bookmarks:archived") + "?q=foo&sort=title_asc"
response.url,
reverse("linkding:bookmarks.archived") + "?q=foo&sort=title_asc",
)
def test_save_search_preferences(self):
@@ -361,7 +366,7 @@ class BookmarkArchivedViewTestCase(
# no params
self.client.post(
reverse("bookmarks:archived"),
reverse("linkding:bookmarks.archived"),
{
"save": "",
},
@@ -378,7 +383,7 @@ class BookmarkArchivedViewTestCase(
# with param
self.client.post(
reverse("bookmarks:archived"),
reverse("linkding:bookmarks.archived"),
{
"save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -396,7 +401,7 @@ class BookmarkArchivedViewTestCase(
# add a param
self.client.post(
reverse("bookmarks:archived"),
reverse("linkding:bookmarks.archived"),
{
"save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -415,7 +420,7 @@ class BookmarkArchivedViewTestCase(
# remove a param
self.client.post(
reverse("bookmarks:archived"),
reverse("linkding:bookmarks.archived"),
{
"save": "",
"unread": BookmarkSearch.FILTER_UNREAD_YES,
@@ -433,7 +438,7 @@ class BookmarkArchivedViewTestCase(
# ignores non-preferences
self.client.post(
reverse("bookmarks:archived"),
reverse("linkding:bookmarks.archived"),
{
"save": "",
"q": "foo",
@@ -453,7 +458,7 @@ class BookmarkArchivedViewTestCase(
)
def test_url_encode_bookmark_actions_url(self):
url = reverse("bookmarks:archived") + "?q=%23foo"
url = reverse("linkding:bookmarks.archived") + "?q=%23foo"
response = self.client.get(url)
html = response.content.decode()
soup = self.make_soup(html)
@@ -467,34 +472,34 @@ class BookmarkArchivedViewTestCase(
def test_encode_search_params(self):
bookmark = self.setup_bookmark(description="alert('xss')", is_archived=True)
url = reverse("bookmarks:archived") + "?q=alert(%27xss%27)"
url = reverse("linkding:bookmarks.archived") + "?q=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
self.assertContains(response, bookmark.url)
url = reverse("bookmarks:archived") + "?sort=alert(%27xss%27)"
url = reverse("linkding:bookmarks.archived") + "?sort=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:archived") + "?unread=alert(%27xss%27)"
url = reverse("linkding:bookmarks.archived") + "?unread=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:archived") + "?shared=alert(%27xss%27)"
url = reverse("linkding:bookmarks.archived") + "?shared=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:archived") + "?user=alert(%27xss%27)"
url = reverse("linkding:bookmarks.archived") + "?user=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:archived") + "?page=alert(%27xss%27)"
url = reverse("linkding:bookmarks.archived") + "?page=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
def test_turbo_frame_details_modal_renders_details_modal_update(self):
bookmark = self.setup_bookmark()
url = reverse("bookmarks:archived") + f"?bookmark_id={bookmark.id}"
url = reverse("linkding:bookmarks.archived") + f"?bookmark_id={bookmark.id}"
response = self.client.get(url, headers={"Turbo-Frame": "details-modal"})
self.assertEqual(200, response.status_code)
@@ -505,7 +510,7 @@ class BookmarkArchivedViewTestCase(
self.assertIsNone(soup.select_one("#tag-cloud-container"))
def test_does_not_include_rss_feed(self):
response = self.client.get(reverse("bookmarks:archived"))
response = self.client.get(reverse("linkding:bookmarks.archived"))
soup = self.make_soup(response.content.decode())
feed = soup.select_one('head link[type="application/rss+xml"]')

View File

@@ -31,7 +31,7 @@ class BookmarkArchivedViewPerformanceTestCase(
# capture number of queries
context = CaptureQueriesContext(self.get_connection())
with context:
response = self.client.get(reverse("bookmarks:archived"))
response = self.client.get(reverse("linkding:bookmarks.archived"))
html = response.content.decode("utf-8")
soup = self.make_soup(html)
list_items = soup.select("li[ld-bookmark-item]")
@@ -46,7 +46,7 @@ class BookmarkArchivedViewPerformanceTestCase(
# assert num queries doesn't increase
with self.assertNumQueries(number_of_queries):
response = self.client.get(reverse("bookmarks:archived"))
response = self.client.get(reverse("linkding:bookmarks.archived"))
html = response.content.decode("utf-8")
soup = self.make_soup(html)
list_items = soup.select("li[ld-bookmark-item]")

View File

@@ -117,13 +117,13 @@ class BookmarkAssetViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(response.status_code, 200)
def test_view_access(self):
self.view_access_test("bookmarks:assets.view")
self.view_access_test("linkding:assets.view")
def test_view_access_guest_user(self):
self.view_access_guest_user_test("bookmarks:assets.view")
self.view_access_guest_user_test("linkding:assets.view")
def test_reader_view_access(self):
self.view_access_test("bookmarks:assets.read")
self.view_access_test("linkding:assets.read")
def test_reader_view_access_guest_user(self):
self.view_access_guest_user_test("bookmarks:assets.read")
self.view_access_guest_user_test("linkding:assets.read")

View File

@@ -43,7 +43,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
]
url = reverse(
"bookmarks:bookmark_asset-list", kwargs={"bookmark_id": bookmark1.id}
"linkding:bookmark_asset-list", kwargs={"bookmark_id": bookmark1.id}
)
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 3)
@@ -52,7 +52,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertAsset(bookmark1_assets[2], response.data["results"][2])
url = reverse(
"bookmarks:bookmark_asset-list", kwargs={"bookmark_id": bookmark2.id}
"linkding:bookmark_asset-list", kwargs={"bookmark_id": bookmark2.id}
)
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 3)
@@ -68,14 +68,14 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_asset(bookmark=bookmark)
url = reverse(
"bookmarks:bookmark_asset-list", kwargs={"bookmark_id": bookmark.id}
"linkding:bookmark_asset-list", kwargs={"bookmark_id": bookmark.id}
)
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
def test_asset_list_requires_authentication(self):
bookmark = self.setup_bookmark()
url = reverse(
"bookmarks:bookmark_asset-list", kwargs={"bookmark_id": bookmark.id}
"linkding:bookmark_asset-list", kwargs={"bookmark_id": bookmark.id}
)
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -94,7 +94,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
gzip=False,
)
url = reverse(
"bookmarks:bookmark_asset-detail",
"linkding:bookmark_asset-detail",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
response = self.get(url, expected_status_code=status.HTTP_200_OK)
@@ -108,7 +108,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
asset = self.setup_asset(bookmark=bookmark)
url = reverse(
"bookmarks:bookmark_asset-detail",
"linkding:bookmark_asset-detail",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
@@ -117,7 +117,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
asset = self.setup_asset(bookmark=bookmark)
url = reverse(
"bookmarks:bookmark_asset-detail",
"linkding:bookmark_asset-detail",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -145,7 +145,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_asset_file(asset=asset, file_content=file_content)
url = reverse(
"bookmarks:bookmark_asset-download",
"linkding:bookmark_asset-download",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
response = self.get(url, expected_status_code=status.HTTP_200_OK)
@@ -173,7 +173,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_asset_file(asset=asset, file_content=file_content)
url = reverse(
"bookmarks:bookmark_asset-download",
"linkding:bookmark_asset-download",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
response = self.get(url, expected_status_code=status.HTTP_200_OK)
@@ -199,7 +199,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
)
url = reverse(
"bookmarks:bookmark_asset-download",
"linkding:bookmark_asset-download",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
@@ -212,7 +212,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
asset = self.setup_asset(bookmark=bookmark)
url = reverse(
"bookmarks:bookmark_asset-download",
"linkding:bookmark_asset-download",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
@@ -221,7 +221,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
asset = self.setup_asset(bookmark=bookmark)
url = reverse(
"bookmarks:bookmark_asset-download",
"linkding:bookmark_asset-download",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -239,7 +239,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
url = reverse(
"bookmarks:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
"linkding:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
)
file_content = b"test file content"
file_name = "test.txt"
@@ -264,7 +264,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
url = reverse(
"bookmarks:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
"linkding:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
)
response = self.client.post(url, {}, format="multipart")
@@ -276,7 +276,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
other_user = self.setup_user()
bookmark = self.setup_bookmark(user=other_user)
url = reverse(
"bookmarks:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
"linkding:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
)
response = self.client.post(url, {}, format="multipart")
@@ -285,7 +285,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def test_upload_asset_requires_authentication(self):
bookmark = self.setup_bookmark()
url = reverse(
"bookmarks:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
"linkding:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
)
response = self.client.post(url, {}, format="multipart")
@@ -296,7 +296,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
bookmark = self.setup_bookmark()
url = reverse(
"bookmarks:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
"linkding:bookmark_asset-upload", kwargs={"bookmark_id": bookmark.id}
)
response = self.client.post(url, {}, format="multipart")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@@ -309,7 +309,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_asset_file(asset=asset)
url = reverse(
"bookmarks:bookmark_asset-detail",
"linkding:bookmark_asset-detail",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
@@ -325,7 +325,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
asset = self.setup_asset(bookmark=bookmark)
url = reverse(
"bookmarks:bookmark_asset-detail",
"linkding:bookmark_asset-detail",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)
@@ -334,7 +334,7 @@ class BookmarkAssetsApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
asset = self.setup_asset(bookmark=bookmark)
url = reverse(
"bookmarks:bookmark_asset-detail",
"linkding:bookmark_asset-detail",
kwargs={"bookmark_id": asset.bookmark.id, "pk": asset.id},
)
self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)

View File

@@ -15,22 +15,26 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
self.client.force_login(user)
def get_details_form(self, soup, bookmark):
form_url = reverse("bookmarks:index.action") + f"?details={bookmark.id}"
form_url = (
reverse("linkding:bookmarks.index.action") + f"?details={bookmark.id}"
)
return soup.find("form", {"action": form_url, "enctype": "multipart/form-data"})
def get_index_details_modal(self, bookmark):
url = reverse("bookmarks:index") + f"?details={bookmark.id}"
url = reverse("linkding:bookmarks.index") + f"?details={bookmark.id}"
response = self.client.get(url)
soup = self.make_soup(response.content)
modal = soup.find("turbo-frame", {"id": "details-modal"})
return modal
soup = self.make_soup(response.content.decode())
return soup.select_one("div.modal.bookmark-details")
def get_shared_details_modal(self, bookmark):
url = reverse("bookmarks:shared") + f"?details={bookmark.id}"
url = reverse("linkding:bookmarks.shared") + f"?details={bookmark.id}"
response = self.client.get(url)
soup = self.make_soup(response.content)
modal = soup.find("turbo-frame", {"id": "details-modal"})
return modal
soup = self.make_soup(response.content.decode())
return soup.select_one("div.modal.bookmark-details")
def has_details_modal(self, response):
soup = self.make_soup(response.content.decode())
return soup.select_one("div.modal.bookmark-details") is not None
def find_section_content(self, soup, section_name):
h3 = soup.find("h3", string=section_name)
@@ -51,59 +55,38 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
def find_asset(self, soup, asset):
return soup.find("div", {"data-asset-id": asset.id})
def details_route_access_test(self):
# own bookmark
bookmark = self.setup_bookmark()
response = self.client.get(
reverse("bookmarks:index") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 200)
# other user's bookmark
other_user = self.setup_user()
bookmark = self.setup_bookmark(user=other_user)
response = self.client.get(
reverse("bookmarks:index") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 404)
# non-existent bookmark - just returns without modal in response
response = self.client.get(reverse("bookmarks:index") + "?details=9999")
self.assertEqual(response.status_code, 200)
# guest user
self.client.logout()
response = self.client.get(
reverse("bookmarks:shared") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 404)
def test_access(self):
# own bookmark
bookmark = self.setup_bookmark()
response = self.client.get(
reverse("bookmarks:index") + f"?details={bookmark.id}"
reverse("linkding:bookmarks.index") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 200)
self.assertTrue(self.has_details_modal(response))
# other user's bookmark
other_user = self.setup_user()
bookmark = self.setup_bookmark(user=other_user)
response = self.client.get(
reverse("bookmarks:index") + f"?details={bookmark.id}"
reverse("linkding:bookmarks.index") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.status_code, 200)
self.assertFalse(self.has_details_modal(response))
# non-existent bookmark - just returns without modal in response
response = self.client.get(reverse("bookmarks:index") + "?details=9999")
response = self.client.get(
reverse("linkding:bookmarks.index") + "?details=9999"
)
self.assertEqual(response.status_code, 200)
self.assertFalse(self.has_details_modal(response))
# guest user
self.client.logout()
response = self.client.get(
reverse("bookmarks:shared") + f"?details={bookmark.id}"
reverse("linkding:bookmarks.shared") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.status_code, 200)
self.assertFalse(self.has_details_modal(response))
def test_access_with_sharing(self):
# shared bookmark, sharing disabled
@@ -111,9 +94,10 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
bookmark = self.setup_bookmark(shared=True, user=other_user)
response = self.client.get(
reverse("bookmarks:shared") + f"?details={bookmark.id}"
reverse("linkding:bookmarks.shared") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.status_code, 200)
self.assertFalse(self.has_details_modal(response))
# shared bookmark, sharing enabled
profile = other_user.profile
@@ -121,25 +105,28 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
profile.save()
response = self.client.get(
reverse("bookmarks:shared") + f"?details={bookmark.id}"
reverse("linkding:bookmarks.shared") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 200)
self.assertTrue(self.has_details_modal(response))
# shared bookmark, guest user, no public sharing
self.client.logout()
response = self.client.get(
reverse("bookmarks:shared") + f"?details={bookmark.id}"
reverse("linkding:bookmarks.shared") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 404)
self.assertEqual(response.status_code, 200)
self.assertFalse(self.has_details_modal(response))
# shared bookmark, guest user, public sharing
profile.enable_public_sharing = True
profile.save()
response = self.client.get(
reverse("bookmarks:shared") + f"?details={bookmark.id}"
reverse("linkding:bookmarks.shared") + f"?details={bookmark.id}"
)
self.assertEqual(response.status_code, 200)
self.assertTrue(self.has_details_modal(response))
def test_displays_title(self):
# with title
@@ -231,7 +218,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
soup = self.get_index_details_modal(bookmark)
self.assertEqual(self.count_weblinks(soup), 3)
reader_mode_url = reverse("bookmarks:assets.read", args=[asset.id])
reader_mode_url = reverse("linkding:assets.read", args=[asset.id])
link = self.find_weblink(soup, reader_mode_url)
self.assertIsNotNone(link)
@@ -465,7 +452,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
for tag in bookmark.tags.all():
tag_link = section.find("a", string=f"#{tag.name}")
self.assertIsNotNone(tag_link)
expected_url = reverse("bookmarks:index") + f"?q=%23{tag.name}"
expected_url = reverse("linkding:bookmarks.index") + f"?q=%23{tag.name}"
self.assertEqual(tag_link["href"], expected_url)
def test_description(self):
@@ -519,7 +506,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
form = delete_button.find_parent("form")
self.assertIsNotNone(form)
expected_url = reverse("bookmarks:index.action")
expected_url = reverse("linkding:bookmarks.index.action")
self.assertEqual(expected_url, form["action"])
def test_actions_visibility(self):
@@ -605,7 +592,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
self.assertIsNotNone(asset_text)
self.assertIn(asset.display_name, asset_text.text)
view_url = reverse("bookmarks:assets.view", args=[asset.id])
view_url = reverse("linkding:assets.view", args=[asset.id])
view_link = asset_item.find("a", {"href": view_url})
self.assertIsNotNone(view_link)
@@ -688,7 +675,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
soup = self.get_index_details_modal(bookmark)
asset_item = self.find_asset(soup, asset)
view_url = reverse("bookmarks:assets.view", args=[asset.id])
view_url = reverse("linkding:assets.view", args=[asset.id])
view_link = asset_item.find("a", {"href": view_url})
self.assertIsNone(view_link)

View File

@@ -28,14 +28,18 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_render_successfully(self):
bookmark = self.setup_bookmark()
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
response = self.client.get(
reverse("linkding:bookmarks.edit", args=[bookmark.id])
)
self.assertEqual(response.status_code, 200)
def test_should_edit_bookmark(self):
bookmark = self.setup_bookmark()
form_data = self.create_form_data({"id": bookmark.id})
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
self.client.post(
reverse("linkding:bookmarks.edit", args=[bookmark.id]), form_data
)
bookmark.refresh_from_db()
@@ -55,7 +59,7 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
form_data = self.create_form_data({"id": bookmark.id, "url": ""})
response = self.client.post(
reverse("bookmarks:edit", args=[bookmark.id]), form_data
reverse("linkding:bookmarks.edit", args=[bookmark.id]), form_data
)
self.assertEqual(response.status_code, 422)
@@ -63,12 +67,16 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
form_data = self.create_form_data({"id": bookmark.id, "unread": True})
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
self.client.post(
reverse("linkding:bookmarks.edit", args=[bookmark.id]), form_data
)
bookmark.refresh_from_db()
self.assertTrue(bookmark.unread)
form_data = self.create_form_data({"id": bookmark.id, "unread": False})
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
self.client.post(
reverse("linkding:bookmarks.edit", args=[bookmark.id]), form_data
)
bookmark.refresh_from_db()
self.assertFalse(bookmark.unread)
@@ -76,12 +84,16 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
form_data = self.create_form_data({"id": bookmark.id, "shared": True})
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
self.client.post(
reverse("linkding:bookmarks.edit", args=[bookmark.id]), form_data
)
bookmark.refresh_from_db()
self.assertTrue(bookmark.shared)
form_data = self.create_form_data({"id": bookmark.id, "shared": False})
self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
self.client.post(
reverse("linkding:bookmarks.edit", args=[bookmark.id]), form_data
)
bookmark.refresh_from_db()
self.assertFalse(bookmark.shared)
@@ -95,7 +107,9 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
notes="edited notes",
)
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
response = self.client.get(
reverse("linkding:bookmarks.edit", args=[bookmark.id])
)
html = response.content.decode()
self.assertInHTML(
@@ -151,21 +165,21 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
# if the URL isn't modified it's not a duplicate
form_data = self.create_form_data({"url": edited_bookmark.url})
response = self.client.post(
reverse("bookmarks:edit", args=[edited_bookmark.id]), form_data
reverse("linkding:bookmarks.edit", args=[edited_bookmark.id]), form_data
)
self.assertEqual(response.status_code, 302)
# if the URL is already bookmarked by another user, it's not a duplicate
form_data = self.create_form_data({"url": other_user_bookmark.url})
response = self.client.post(
reverse("bookmarks:edit", args=[edited_bookmark.id]), form_data
reverse("linkding:bookmarks.edit", args=[edited_bookmark.id]), form_data
)
self.assertEqual(response.status_code, 302)
# if the URL is already bookmarked by the same user, it's a duplicate
form_data = self.create_form_data({"url": existing_bookmark.url})
response = self.client.post(
reverse("bookmarks:edit", args=[edited_bookmark.id]), form_data
reverse("linkding:bookmarks.edit", args=[edited_bookmark.id]), form_data
)
self.assertEqual(response.status_code, 422)
self.assertInHTML(
@@ -180,23 +194,23 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
form_data = self.create_form_data()
url = (
reverse("bookmarks:edit", args=[bookmark.id])
reverse("linkding:bookmarks.edit", args=[bookmark.id])
+ "?return_url="
+ reverse("bookmarks:close")
+ reverse("linkding:bookmarks.close")
)
response = self.client.post(url, form_data)
self.assertRedirects(response, reverse("bookmarks:close"))
self.assertRedirects(response, reverse("linkding:bookmarks.close"))
def test_should_redirect_to_bookmark_index_by_default(self):
bookmark = self.setup_bookmark()
form_data = self.create_form_data()
response = self.client.post(
reverse("bookmarks:edit", args=[bookmark.id]), form_data
reverse("linkding:bookmarks.edit", args=[bookmark.id]), form_data
)
self.assertRedirects(response, reverse("bookmarks:index"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
def test_should_not_redirect_to_external_url(self):
bookmark = self.setup_bookmark()
@@ -204,17 +218,17 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
def post_with(return_url, follow=None):
form_data = self.create_form_data()
url = (
reverse("bookmarks:edit", args=[bookmark.id])
reverse("linkding:bookmarks.edit", args=[bookmark.id])
+ f"?return_url={return_url}"
)
return self.client.post(url, form_data, follow=follow)
response = post_with("https://example.com")
self.assertRedirects(response, reverse("bookmarks:index"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
response = post_with("//example.com")
self.assertRedirects(response, reverse("bookmarks:index"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
response = post_with("://example.com")
self.assertRedirects(response, reverse("bookmarks:index"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
response = post_with("/foo//example.com", follow=True)
self.assertEqual(response.status_code, 404)
@@ -227,7 +241,7 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
form_data = self.create_form_data({"id": bookmark.id})
response = self.client.post(
reverse("bookmarks:edit", args=[bookmark.id]), form_data
reverse("linkding:bookmarks.edit", args=[bookmark.id]), form_data
)
bookmark.refresh_from_db()
self.assertNotEqual(bookmark.url, form_data["url"])
@@ -238,7 +252,9 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
self.user.profile.enable_sharing = False
self.user.profile.save()
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
response = self.client.get(
reverse("linkding:bookmarks.edit", args=[bookmark.id])
)
html = response.content.decode()
self.assertInHTML(
@@ -255,7 +271,9 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
self.user.profile.enable_sharing = True
self.user.profile.save()
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
response = self.client.get(
reverse("linkding:bookmarks.edit", args=[bookmark.id])
)
html = response.content.decode()
self.assertInHTML(
@@ -272,12 +290,16 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_hide_notes_if_there_are_no_notes(self):
bookmark = self.setup_bookmark()
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
response = self.client.get(
reverse("linkding:bookmarks.edit", args=[bookmark.id])
)
self.assertContains(response, '<details class="notes">', count=1)
def test_should_show_notes_if_there_are_notes(self):
bookmark = self.setup_bookmark(notes="test notes")
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
response = self.client.get(
reverse("linkding:bookmarks.edit", args=[bookmark.id])
)
self.assertContains(response, '<details class="notes" open>', count=1)

View File

@@ -44,7 +44,7 @@ class BookmarkIndexViewTestCase(
self.setup_bookmark(user=other_user),
]
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks)
@@ -53,7 +53,7 @@ class BookmarkIndexViewTestCase(
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix="foo")
invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix="bar")
response = self.client.get(reverse("bookmarks:index") + "?q=foo")
response = self.client.get(reverse("linkding:bookmarks.index") + "?q=foo")
self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks)
@@ -75,7 +75,7 @@ class BookmarkIndexViewTestCase(
archived_bookmarks + other_user_bookmarks
)
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
@@ -91,7 +91,7 @@ class BookmarkIndexViewTestCase(
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks)
response = self.client.get(reverse("bookmarks:index") + "?q=foo")
response = self.client.get(reverse("linkding:bookmarks.index") + "?q=foo")
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
@@ -113,7 +113,7 @@ class BookmarkIndexViewTestCase(
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
read_tags = self.get_tags_from_bookmarks(read_bookmarks)
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
self.assertVisibleBookmarks(response, unread_bookmarks)
self.assertInvisibleBookmarks(response, read_bookmarks)
self.assertVisibleTags(response, unread_tags)
@@ -130,7 +130,7 @@ class BookmarkIndexViewTestCase(
self.setup_bookmark(tags=tags)
response = self.client.get(
reverse("bookmarks:index")
reverse("linkding:bookmarks.index")
+ f"?q=%23{tags[0].name}+%23{tags[1].name.upper()}"
)
@@ -149,7 +149,8 @@ class BookmarkIndexViewTestCase(
self.setup_bookmark(title=tags[0].name, tags=tags)
response = self.client.get(
reverse("bookmarks:index") + f"?q={tags[0].name}+%23{tags[1].name.upper()}"
reverse("linkding:bookmarks.index")
+ f"?q={tags[0].name}+%23{tags[1].name.upper()}"
)
self.assertSelectedTags(response, [tags[1]])
@@ -168,7 +169,8 @@ class BookmarkIndexViewTestCase(
self.setup_bookmark(tags=tags)
response = self.client.get(
reverse("bookmarks:index") + f"?q={tags[0].name}+%23{tags[1].name.upper()}"
reverse("linkding:bookmarks.index")
+ f"?q={tags[0].name}+%23{tags[1].name.upper()}"
)
self.assertSelectedTags(response, [tags[0], tags[1]])
@@ -176,7 +178,7 @@ class BookmarkIndexViewTestCase(
def test_should_open_bookmarks_in_new_page_by_default(self):
visible_bookmarks = self.setup_numbered_bookmarks(3)
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
@@ -187,14 +189,14 @@ class BookmarkIndexViewTestCase(
visible_bookmarks = self.setup_numbered_bookmarks(3)
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
def test_edit_link_return_url_respects_search_options(self):
bookmark = self.setup_bookmark(title="foo")
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
base_url = reverse("bookmarks:index")
edit_url = reverse("linkding:bookmarks.edit", args=[bookmark.id])
base_url = reverse("linkding:bookmarks.index")
# without query params
return_url = urllib.parse.quote(base_url)
@@ -220,8 +222,8 @@ class BookmarkIndexViewTestCase(
self.assertEditLink(response, url)
def test_bulk_edit_respects_search_options(self):
action_url = reverse("bookmarks:index.action")
base_url = reverse("bookmarks:index")
action_url = reverse("linkding:bookmarks.index.action")
base_url = reverse("linkding:bookmarks.index")
# without params
url = f"{action_url}"
@@ -244,7 +246,7 @@ class BookmarkIndexViewTestCase(
self.assertBulkActionForm(response, url)
def test_allowed_bulk_actions(self):
url = reverse("bookmarks:index")
url = reverse("linkding:bookmarks.index")
response = self.client.get(url)
html = response.content.decode()
@@ -257,6 +259,7 @@ class BookmarkIndexViewTestCase(
<option value="bulk_untag">Remove tags</option>
<option value="bulk_read">Mark as read</option>
<option value="bulk_unread">Mark as unread</option>
<option value="bulk_refresh">Refresh from website</option>
</select>
""",
html,
@@ -267,7 +270,7 @@ class BookmarkIndexViewTestCase(
user_profile.enable_sharing = True
user_profile.save()
url = reverse("bookmarks:index")
url = reverse("linkding:bookmarks.index")
response = self.client.get(url)
html = response.content.decode()
@@ -282,6 +285,7 @@ class BookmarkIndexViewTestCase(
<option value="bulk_unread">Mark as unread</option>
<option value="bulk_share">Share</option>
<option value="bulk_unshare">Unshare</option>
<option value="bulk_refresh">Refresh from website</option>
</select>
""",
html,
@@ -289,13 +293,13 @@ class BookmarkIndexViewTestCase(
def test_apply_search_preferences(self):
# no params
response = self.client.post(reverse("bookmarks:index"))
response = self.client.post(reverse("linkding:bookmarks.index"))
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("bookmarks:index"))
self.assertEqual(response.url, reverse("linkding:bookmarks.index"))
# some params
response = self.client.post(
reverse("bookmarks:index"),
reverse("linkding:bookmarks.index"),
{
"q": "foo",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -303,12 +307,12 @@ class BookmarkIndexViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, reverse("bookmarks:index") + "?q=foo&sort=title_asc"
response.url, reverse("linkding:bookmarks.index") + "?q=foo&sort=title_asc"
)
# params with default value are removed
response = self.client.post(
reverse("bookmarks:index"),
reverse("linkding:bookmarks.index"),
{
"q": "foo",
"user": "",
@@ -318,11 +322,13 @@ class BookmarkIndexViewTestCase(
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("bookmarks:index") + "?q=foo&unread=yes")
self.assertEqual(
response.url, reverse("linkding:bookmarks.index") + "?q=foo&unread=yes"
)
# page is removed
response = self.client.post(
reverse("bookmarks:index"),
reverse("linkding:bookmarks.index"),
{
"q": "foo",
"page": "2",
@@ -331,7 +337,7 @@ class BookmarkIndexViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, reverse("bookmarks:index") + "?q=foo&sort=title_asc"
response.url, reverse("linkding:bookmarks.index") + "?q=foo&sort=title_asc"
)
def test_save_search_preferences(self):
@@ -339,7 +345,7 @@ class BookmarkIndexViewTestCase(
# no params
self.client.post(
reverse("bookmarks:index"),
reverse("linkding:bookmarks.index"),
{
"save": "",
},
@@ -356,7 +362,7 @@ class BookmarkIndexViewTestCase(
# with param
self.client.post(
reverse("bookmarks:index"),
reverse("linkding:bookmarks.index"),
{
"save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -374,7 +380,7 @@ class BookmarkIndexViewTestCase(
# add a param
self.client.post(
reverse("bookmarks:index"),
reverse("linkding:bookmarks.index"),
{
"save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -393,7 +399,7 @@ class BookmarkIndexViewTestCase(
# remove a param
self.client.post(
reverse("bookmarks:index"),
reverse("linkding:bookmarks.index"),
{
"save": "",
"unread": BookmarkSearch.FILTER_UNREAD_YES,
@@ -411,7 +417,7 @@ class BookmarkIndexViewTestCase(
# ignores non-preferences
self.client.post(
reverse("bookmarks:index"),
reverse("linkding:bookmarks.index"),
{
"save": "",
"q": "foo",
@@ -431,7 +437,7 @@ class BookmarkIndexViewTestCase(
)
def test_url_encode_bookmark_actions_url(self):
url = reverse("bookmarks:index") + "?q=%23foo"
url = reverse("linkding:bookmarks.index") + "?q=%23foo"
response = self.client.get(url)
html = response.content.decode()
soup = self.make_soup(html)
@@ -445,34 +451,34 @@ class BookmarkIndexViewTestCase(
def test_encode_search_params(self):
bookmark = self.setup_bookmark(description="alert('xss')")
url = reverse("bookmarks:index") + "?q=alert(%27xss%27)"
url = reverse("linkding:bookmarks.index") + "?q=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
self.assertContains(response, bookmark.url)
url = reverse("bookmarks:index") + "?sort=alert(%27xss%27)"
url = reverse("linkding:bookmarks.index") + "?sort=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:index") + "?unread=alert(%27xss%27)"
url = reverse("linkding:bookmarks.index") + "?unread=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:index") + "?shared=alert(%27xss%27)"
url = reverse("linkding:bookmarks.index") + "?shared=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:index") + "?user=alert(%27xss%27)"
url = reverse("linkding:bookmarks.index") + "?user=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:index") + "?page=alert(%27xss%27)"
url = reverse("linkding:bookmarks.index") + "?page=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
def test_turbo_frame_details_modal_renders_details_modal_update(self):
bookmark = self.setup_bookmark()
url = reverse("bookmarks:index") + f"?bookmark_id={bookmark.id}"
url = reverse("linkding:bookmarks.index") + f"?bookmark_id={bookmark.id}"
response = self.client.get(url, headers={"Turbo-Frame": "details-modal"})
self.assertEqual(200, response.status_code)
@@ -483,7 +489,7 @@ class BookmarkIndexViewTestCase(
self.assertIsNone(soup.select_one("#tag-cloud-container"))
def test_does_not_include_rss_feed(self):
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
soup = self.make_soup(response.content.decode())
feed = soup.select_one('head link[type="application/rss+xml"]')

View File

@@ -31,7 +31,7 @@ class BookmarkIndexViewPerformanceTestCase(
# capture number of queries
context = CaptureQueriesContext(self.get_connection())
with context:
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode("utf-8")
soup = self.make_soup(html)
list_items = soup.select("li[ld-bookmark-item]")
@@ -46,7 +46,7 @@ class BookmarkIndexViewPerformanceTestCase(
# assert num queries doesn't increase
with self.assertNumQueries(number_of_queries):
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode("utf-8")
soup = self.make_soup(html)
list_items = soup.select("li[ld-bookmark-item]")

View File

@@ -29,7 +29,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_create_new_bookmark(self):
form_data = self.create_form_data()
self.client.post(reverse("bookmarks:new"), form_data)
self.client.post(reverse("linkding:bookmarks.new"), form_data)
self.assertEqual(Bookmark.objects.count(), 1)
@@ -48,13 +48,13 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_return_422_with_invalid_form(self):
form_data = self.create_form_data({"url": ""})
response = self.client.post(reverse("bookmarks:new"), form_data)
response = self.client.post(reverse("linkding:bookmarks.new"), form_data)
self.assertEqual(response.status_code, 422)
def test_should_create_new_unread_bookmark(self):
form_data = self.create_form_data({"unread": True})
self.client.post(reverse("bookmarks:new"), form_data)
self.client.post(reverse("linkding:bookmarks.new"), form_data)
self.assertEqual(Bookmark.objects.count(), 1)
@@ -64,7 +64,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_create_new_shared_bookmark(self):
form_data = self.create_form_data({"shared": True})
self.client.post(reverse("bookmarks:new"), form_data)
self.client.post(reverse("linkding:bookmarks.new"), form_data)
self.assertEqual(Bookmark.objects.count(), 1)
@@ -72,7 +72,9 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(bookmark.shared)
def test_should_prefill_url_from_url_parameter(self):
response = self.client.get(reverse("bookmarks:new") + "?url=http://example.com")
response = self.client.get(
reverse("linkding:bookmarks.new") + "?url=http://example.com"
)
html = response.content.decode()
self.assertInHTML(
@@ -83,7 +85,9 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
)
def test_should_prefill_title_from_url_parameter(self):
response = self.client.get(reverse("bookmarks:new") + "?title=Example%20Title")
response = self.client.get(
reverse("linkding:bookmarks.new") + "?title=Example%20Title"
)
html = response.content.decode()
self.assertInHTML(
@@ -95,7 +99,8 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_prefill_description_from_url_parameter(self):
response = self.client.get(
reverse("bookmarks:new") + "?description=Example%20Site%20Description"
reverse("linkding:bookmarks.new")
+ "?description=Example%20Site%20Description"
)
html = response.content.decode()
@@ -105,9 +110,22 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
html,
)
def test_should_prefill_tags_from_url_parameter(self):
response = self.client.get(
reverse("linkding:bookmarks.new") + "?tags=tag1%20tag2%20tag3"
)
html = response.content.decode()
self.assertInHTML(
'<input type="text" name="tag_string" value="tag1 tag2 tag3" '
'class="form-input" autocomplete="off" autocapitalize="off" '
'id="id_tag_string">',
html,
)
def test_should_prefill_notes_from_url_parameter(self):
response = self.client.get(
reverse("bookmarks:new")
reverse("linkding:bookmarks.new")
+ "?notes=%2A%2AFind%2A%2A%20more%20info%20%5Bhere%5D%28http%3A%2F%2Fexample.com%29"
)
html = response.content.decode()
@@ -129,50 +147,51 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
)
def test_should_enable_auto_close_when_specified_in_url_parameter(self):
response = self.client.get(reverse("bookmarks:new") + "?auto_close")
response = self.client.get(reverse("linkding:bookmarks.new") + "?auto_close")
html = response.content.decode()
self.assertInHTML(
'<input type="hidden" name="auto_close" value="true" '
'id="id_auto_close">',
'<input type="hidden" name="auto_close" value="True" id="id_auto_close">',
html,
)
def test_should_not_enable_auto_close_when_not_specified_in_url_parameter(self):
response = self.client.get(reverse("bookmarks:new"))
response = self.client.get(reverse("linkding:bookmarks.new"))
html = response.content.decode()
self.assertInHTML(
'<input type="hidden" name="auto_close" id="id_auto_close">', html
'<input type="hidden" name="auto_close" value="False" id="id_auto_close">',
html,
)
def test_should_redirect_to_index_view(self):
form_data = self.create_form_data()
response = self.client.post(reverse("bookmarks:new"), form_data)
response = self.client.post(reverse("linkding:bookmarks.new"), form_data)
self.assertRedirects(response, reverse("bookmarks:index"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
def test_should_not_redirect_to_external_url(self):
form_data = self.create_form_data()
response = self.client.post(
reverse("bookmarks:new") + "?return_url=https://example.com", form_data
reverse("linkding:bookmarks.new") + "?return_url=https://example.com",
form_data,
)
self.assertRedirects(response, reverse("bookmarks:index"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
def test_auto_close_should_redirect_to_close_view(self):
form_data = self.create_form_data({"auto_close": "true"})
form_data = self.create_form_data({"auto_close": "True"})
response = self.client.post(reverse("bookmarks:new"), form_data)
response = self.client.post(reverse("linkding:bookmarks.new"), form_data)
self.assertRedirects(response, reverse("bookmarks:close"))
self.assertRedirects(response, reverse("linkding:bookmarks.close"))
def test_should_respect_share_profile_setting(self):
self.user.profile.enable_sharing = False
self.user.profile.save()
response = self.client.get(reverse("bookmarks:new"))
response = self.client.get(reverse("linkding:bookmarks.new"))
html = response.content.decode()
self.assertInHTML(
@@ -189,7 +208,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
self.user.profile.enable_sharing = True
self.user.profile.save()
response = self.client.get(reverse("bookmarks:new"))
response = self.client.get(reverse("linkding:bookmarks.new"))
html = response.content.decode()
self.assertInHTML(
@@ -208,7 +227,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
self.user.profile.enable_sharing = True
self.user.profile.save()
response = self.client.get(reverse("bookmarks:new"))
response = self.client.get(reverse("linkding:bookmarks.new"))
html = response.content.decode()
self.assertInHTML(
"""
@@ -222,7 +241,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
self.user.profile.enable_public_sharing = True
self.user.profile.save()
response = self.client.get(reverse("bookmarks:new"))
response = self.client.get(reverse("linkding:bookmarks.new"))
html = response.content.decode()
self.assertInHTML(
"""
@@ -235,12 +254,14 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_hide_notes_if_there_are_no_notes(self):
bookmark = self.setup_bookmark()
response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
response = self.client.get(
reverse("linkding:bookmarks.edit", args=[bookmark.id])
)
self.assertContains(response, '<details class="notes">', count=1)
def test_should_not_check_unread_by_default(self):
response = self.client.get(reverse("bookmarks:new"))
response = self.client.get(reverse("linkding:bookmarks.new"))
html = response.content.decode()
self.assertInHTML(
@@ -252,11 +273,10 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
self.user.profile.default_mark_unread = True
self.user.profile.save()
response = self.client.get(reverse("bookmarks:new"))
response = self.client.get(reverse("linkding:bookmarks.new"))
html = response.content.decode()
self.assertInHTML(
'<input type="checkbox" name="unread" value="true" '
'id="id_unread" checked="">',
'<input type="checkbox" name="unread" id="id_unread" checked="">',
html,
)

View File

@@ -75,7 +75,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=True, user=user4),
]
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks)
@@ -94,7 +94,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=True, user=user3),
]
url = reverse("bookmarks:shared") + "?user=" + user1.username
url = reverse("linkding:bookmarks.shared") + "?user=" + user1.username
response = self.client.get(url)
self.assertVisibleBookmarks(response, visible_bookmarks)
@@ -109,7 +109,7 @@ class BookmarkSharedViewTestCase(
)
invisible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user)
response = self.client.get(reverse("bookmarks:shared") + "?q=foo")
response = self.client.get(reverse("linkding:bookmarks.shared") + "?q=foo")
self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks)
@@ -125,7 +125,7 @@ class BookmarkSharedViewTestCase(
3, shared=True, user=user2, prefix="user2"
)
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks)
@@ -159,7 +159,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=False, user=user3, tags=[invisible_tags[2]])
self.setup_bookmark(shared=True, user=user4, tags=[invisible_tags[3]])
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
@@ -181,7 +181,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[1]])
url = reverse("bookmarks:shared") + "?user=" + user1.username
url = reverse("linkding:bookmarks.shared") + "?user=" + user1.username
response = self.client.get(url)
self.assertVisibleTags(response, visible_tags)
@@ -217,7 +217,9 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[2]])
response = self.client.get(reverse("bookmarks:shared") + "?q=searchvalue")
response = self.client.get(
reverse("linkding:bookmarks.shared") + "?q=searchvalue"
)
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
@@ -241,7 +243,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags)
@@ -258,7 +260,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=False, user=self.setup_user(enable_sharing=True))
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=False))
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleUserOptions(response, expected_visible_users)
def test_should_list_only_users_with_publicly_shared_bookmarks_without_login(self):
@@ -278,7 +280,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=True))
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=True))
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleUserOptions(response, expected_visible_users)
def test_should_list_bookmarks_and_tags_for_search_preferences(self):
@@ -313,7 +315,7 @@ class BookmarkSharedViewTestCase(
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
read_tags = self.get_tags_from_bookmarks(read_bookmarks)
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleBookmarks(response, unread_bookmarks)
self.assertInvisibleBookmarks(response, read_bookmarks)
self.assertVisibleTags(response, unread_tags)
@@ -330,7 +332,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=True),
]
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
@@ -347,7 +349,7 @@ class BookmarkSharedViewTestCase(
self.setup_bookmark(shared=True),
]
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
@@ -358,8 +360,8 @@ class BookmarkSharedViewTestCase(
user.profile.save()
bookmark = self.setup_bookmark(title="foo", shared=True, user=user)
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
base_url = reverse("bookmarks:shared")
edit_url = reverse("linkding:bookmarks.edit", args=[bookmark.id])
base_url = reverse("linkding:bookmarks.shared")
# without query params
return_url = urllib.parse.quote(base_url)
@@ -394,13 +396,13 @@ class BookmarkSharedViewTestCase(
def test_apply_search_preferences(self):
# no params
response = self.client.post(reverse("bookmarks:shared"))
response = self.client.post(reverse("linkding:bookmarks.shared"))
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("bookmarks:shared"))
self.assertEqual(response.url, reverse("linkding:bookmarks.shared"))
# some params
response = self.client.post(
reverse("bookmarks:shared"),
reverse("linkding:bookmarks.shared"),
{
"q": "foo",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -408,12 +410,12 @@ class BookmarkSharedViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, reverse("bookmarks:shared") + "?q=foo&sort=title_asc"
response.url, reverse("linkding:bookmarks.shared") + "?q=foo&sort=title_asc"
)
# params with default value are removed
response = self.client.post(
reverse("bookmarks:shared"),
reverse("linkding:bookmarks.shared"),
{
"q": "foo",
"user": "",
@@ -424,12 +426,12 @@ class BookmarkSharedViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, reverse("bookmarks:shared") + "?q=foo&unread=yes"
response.url, reverse("linkding:bookmarks.shared") + "?q=foo&unread=yes"
)
# page is removed
response = self.client.post(
reverse("bookmarks:shared"),
reverse("linkding:bookmarks.shared"),
{
"q": "foo",
"page": "2",
@@ -438,7 +440,7 @@ class BookmarkSharedViewTestCase(
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, reverse("bookmarks:shared") + "?q=foo&sort=title_asc"
response.url, reverse("linkding:bookmarks.shared") + "?q=foo&sort=title_asc"
)
def test_save_search_preferences(self):
@@ -447,7 +449,7 @@ class BookmarkSharedViewTestCase(
# no params
self.client.post(
reverse("bookmarks:shared"),
reverse("linkding:bookmarks.shared"),
{
"save": "",
},
@@ -464,7 +466,7 @@ class BookmarkSharedViewTestCase(
# with param
self.client.post(
reverse("bookmarks:shared"),
reverse("linkding:bookmarks.shared"),
{
"save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -482,7 +484,7 @@ class BookmarkSharedViewTestCase(
# add a param
self.client.post(
reverse("bookmarks:shared"),
reverse("linkding:bookmarks.shared"),
{
"save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
@@ -501,7 +503,7 @@ class BookmarkSharedViewTestCase(
# remove a param
self.client.post(
reverse("bookmarks:shared"),
reverse("linkding:bookmarks.shared"),
{
"save": "",
"unread": BookmarkSearch.FILTER_UNREAD_YES,
@@ -519,7 +521,7 @@ class BookmarkSharedViewTestCase(
# ignores non-preferences
self.client.post(
reverse("bookmarks:shared"),
reverse("linkding:bookmarks.shared"),
{
"save": "",
"q": "foo",
@@ -539,7 +541,7 @@ class BookmarkSharedViewTestCase(
)
def test_url_encode_bookmark_actions_url(self):
url = reverse("bookmarks:shared") + "?q=%23foo"
url = reverse("linkding:bookmarks.shared") + "?q=%23foo"
response = self.client.get(url)
html = response.content.decode()
soup = self.make_soup(html)
@@ -557,34 +559,34 @@ class BookmarkSharedViewTestCase(
user.profile.save()
bookmark = self.setup_bookmark(description="alert('xss')", shared=True)
url = reverse("bookmarks:shared") + "?q=alert(%27xss%27)"
url = reverse("linkding:bookmarks.shared") + "?q=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
self.assertContains(response, bookmark.url)
url = reverse("bookmarks:shared") + "?sort=alert(%27xss%27)"
url = reverse("linkding:bookmarks.shared") + "?sort=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:shared") + "?unread=alert(%27xss%27)"
url = reverse("linkding:bookmarks.shared") + "?unread=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:shared") + "?shared=alert(%27xss%27)"
url = reverse("linkding:bookmarks.shared") + "?shared=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:shared") + "?user=alert(%27xss%27)"
url = reverse("linkding:bookmarks.shared") + "?user=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
url = reverse("bookmarks:shared") + "?page=alert(%27xss%27)"
url = reverse("linkding:bookmarks.shared") + "?page=alert(%27xss%27)"
response = self.client.get(url)
self.assertNotContains(response, "alert('xss')")
def test_turbo_frame_details_modal_renders_details_modal_update(self):
bookmark = self.setup_bookmark()
url = reverse("bookmarks:shared") + f"?bookmark_id={bookmark.id}"
url = reverse("linkding:bookmarks.shared") + f"?bookmark_id={bookmark.id}"
response = self.client.get(url, headers={"Turbo-Frame": "details-modal"})
self.assertEqual(200, response.status_code)
@@ -595,9 +597,9 @@ class BookmarkSharedViewTestCase(
self.assertIsNone(soup.select_one("#tag-cloud-container"))
def test_includes_public_shared_rss_feed(self):
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
soup = self.make_soup(response.content.decode())
feed = soup.select_one('head link[type="application/rss+xml"]')
self.assertIsNotNone(feed)
self.assertEqual(feed.attrs["href"], reverse("bookmarks:feeds.public_shared"))
self.assertEqual(feed.attrs["href"], reverse("linkding:feeds.public_shared"))

View File

@@ -32,7 +32,7 @@ class BookmarkSharedViewPerformanceTestCase(
# capture number of queries
context = CaptureQueriesContext(self.get_connection())
with context:
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
html = response.content.decode("utf-8")
soup = self.make_soup(html)
list_items = soup.select("li[ld-bookmark-item]")
@@ -48,7 +48,7 @@ class BookmarkSharedViewPerformanceTestCase(
# assert num queries doesn't increase
with self.assertNumQueries(number_of_queries):
response = self.client.get(reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:bookmarks.shared"))
html = response.content.decode("utf-8")
soup = self.make_soup(html)
list_items = soup.select("li[ld-bookmark-item]")

View File

@@ -1,12 +1,12 @@
import datetime
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from django.test.client import RequestFactory
from bookmarks.models import BookmarkForm, Bookmark
User = get_user_model()
from bookmarks.forms import BookmarkForm
from bookmarks.models import Bookmark
from bookmarks.tests.helpers import BookmarkFactoryMixin
ENABLED_URL_VALIDATION_TEST_CASES = [
("thisisnotavalidurl", False),
@@ -29,12 +29,10 @@ DISABLED_URL_VALIDATION_TEST_CASES = [
]
class BookmarkValidationTestCase(TestCase):
class BookmarkValidationTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self) -> None:
self.user = User.objects.create_user(
"testuser", "test@example.com", "password123"
)
self.get_or_create_test_user()
def test_bookmark_model_should_not_allow_missing_url(self):
bookmark = Bookmark(
@@ -66,12 +64,15 @@ class BookmarkValidationTestCase(TestCase):
self._run_bookmark_model_url_validity_checks(DISABLED_URL_VALIDATION_TEST_CASES)
def test_bookmark_form_should_validate_required_fields(self):
form = BookmarkForm(data={"url": ""})
rf = RequestFactory()
request = rf.post("/", data={"url": ""})
form = BookmarkForm(request)
self.assertEqual(len(form.errors), 1)
self.assertIn("required", str(form.errors))
form = BookmarkForm(data={"url": None})
request = rf.post("/", data={})
form = BookmarkForm(request)
self.assertEqual(len(form.errors), 1)
self.assertIn("required", str(form.errors))
@@ -106,7 +107,9 @@ class BookmarkValidationTestCase(TestCase):
def _run_bookmark_form_url_validity_checks(self, cases):
for case in cases:
url, expectation = case
form = BookmarkForm(data={"url": url})
rf = RequestFactory()
request = rf.post("/", data={"url": url})
form = BookmarkForm(request)
if expectation:
self.assertEqual(len(form.errors), 0)

View File

@@ -89,7 +89,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmarks = self.setup_numbered_bookmarks(5)
response = self.get(
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
reverse("linkding:bookmark-list"), expected_status_code=status.HTTP_200_OK
)
self.assertBookmarkListEqual(response.data["results"], bookmarks)
@@ -104,7 +104,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
)
response = self.get(
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
reverse("linkding:bookmark-list"), expected_status_code=status.HTTP_200_OK
)
self.assertBookmarkListEqual(response.data["results"], bookmarks)
@@ -116,7 +116,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark.save()
response = self.get(
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
reverse("linkding:bookmark-list"), expected_status_code=status.HTTP_200_OK
)
self.assertIsNone(response.data["results"][0]["website_title"])
self.assertIsNone(response.data["results"][0]["website_description"])
@@ -127,7 +127,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_numbered_bookmarks(5, archived=True)
response = self.get(
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
reverse("linkding:bookmark-list"), expected_status_code=status.HTTP_200_OK
)
self.assertBookmarkListEqual(response.data["results"], bookmarks)
@@ -138,7 +138,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_numbered_bookmarks(5)
response = self.get(
reverse("bookmarks:bookmark-list") + "?q=" + search_value,
reverse("linkding:bookmark-list") + "?q=" + search_value,
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], bookmarks)
@@ -150,7 +150,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
# Filter off
response = self.get(
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
reverse("linkding:bookmark-list"), expected_status_code=status.HTTP_200_OK
)
self.assertBookmarkListEqual(
response.data["results"], unread_bookmarks + read_bookmarks
@@ -158,14 +158,14 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
# Filter shared
response = self.get(
reverse("bookmarks:bookmark-list") + "?unread=yes",
reverse("linkding:bookmark-list") + "?unread=yes",
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], unread_bookmarks)
# Filter unshared
response = self.get(
reverse("bookmarks:bookmark-list") + "?unread=no",
reverse("linkding:bookmark-list") + "?unread=no",
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], read_bookmarks)
@@ -177,7 +177,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
# Filter off
response = self.get(
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
reverse("linkding:bookmark-list"), expected_status_code=status.HTTP_200_OK
)
self.assertBookmarkListEqual(
response.data["results"], unshared_bookmarks + shared_bookmarks
@@ -185,14 +185,14 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
# Filter shared
response = self.get(
reverse("bookmarks:bookmark-list") + "?shared=yes",
reverse("linkding:bookmark-list") + "?shared=yes",
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], shared_bookmarks)
# Filter unshared
response = self.get(
reverse("bookmarks:bookmark-list") + "?shared=no",
reverse("linkding:bookmark-list") + "?shared=no",
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], unshared_bookmarks)
@@ -203,7 +203,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmarks.reverse()
response = self.get(
reverse("bookmarks:bookmark-list") + "?sort=title_desc",
reverse("linkding:bookmark-list") + "?sort=title_desc",
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], bookmarks)
@@ -214,7 +214,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
archived_bookmarks = self.setup_numbered_bookmarks(5, archived=True)
response = self.get(
reverse("bookmarks:bookmark-archived"),
reverse("linkding:bookmark-archived"),
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], archived_bookmarks)
@@ -231,7 +231,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
)
response = self.get(
reverse("bookmarks:bookmark-archived"),
reverse("linkding:bookmark-archived"),
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], archived_bookmarks)
@@ -245,7 +245,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_numbered_bookmarks(5, archived=True)
response = self.get(
reverse("bookmarks:bookmark-archived") + "?q=" + search_value,
reverse("linkding:bookmark-archived") + "?q=" + search_value,
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], archived_bookmarks)
@@ -256,7 +256,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmarks.reverse()
response = self.get(
reverse("bookmarks:bookmark-archived") + "?sort=title_desc",
reverse("linkding:bookmark-archived") + "?sort=title_desc",
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], bookmarks)
@@ -280,7 +280,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_bookmark(shared=True, user=user4)
response = self.get(
reverse("bookmarks:bookmark-shared"),
reverse("linkding:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], shared_bookmarks)
@@ -300,7 +300,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
)
response = self.get(
reverse("bookmarks:bookmark-shared"),
reverse("linkding:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], shared_bookmarks)
@@ -317,7 +317,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_bookmark(shared=True, user=user2)
response = self.get(
reverse("bookmarks:bookmark-shared"),
reverse("linkding:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], shared_bookmarks)
@@ -339,7 +339,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_bookmark(shared=True, user=user3),
response = self.get(
reverse("bookmarks:bookmark-shared") + "?q=searchvalue",
reverse("linkding:bookmark-shared") + "?q=searchvalue",
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], expected_bookmarks)
@@ -352,7 +352,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.setup_bookmark(shared=True, user=user_search_user),
]
response = self.get(
reverse("bookmarks:bookmark-shared") + "?user=" + user_search_user.username,
reverse("linkding:bookmark-shared") + "?user=" + user_search_user.username,
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], expected_bookmarks)
@@ -371,7 +371,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
),
]
response = self.get(
reverse("bookmarks:bookmark-shared")
reverse("linkding:bookmark-shared")
+ "?q=searchvalue&user="
+ combined_search_user.username,
expected_status_code=status.HTTP_200_OK,
@@ -385,7 +385,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmarks.reverse()
response = self.get(
reverse("bookmarks:bookmark-shared") + "?sort=title_desc",
reverse("linkding:bookmark-shared") + "?sort=title_desc",
expected_status_code=status.HTTP_200_OK,
)
self.assertBookmarkListEqual(response.data["results"], bookmarks)
@@ -403,7 +403,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
"shared": False,
"tag_names": ["tag1", "tag2"],
}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertEqual(bookmark.url, data["url"])
self.assertEqual(bookmark.title, data["title"])
@@ -427,7 +427,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
description="Website description",
preview_image=None,
)
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertEqual(bookmark.title, "Website title")
self.assertEqual(bookmark.description, "Website description")
@@ -446,7 +446,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
preview_image=None,
)
self.post(
reverse("bookmarks:bookmark-list") + "?disable_scraping",
reverse("linkding:bookmark-list") + "?disable_scraping",
data,
status.HTTP_201_CREATED,
)
@@ -463,7 +463,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
wraps=bookmarks.services.bookmarks.create_bookmark,
) as mock_create_bookmark:
data = {"url": "https://example.com/"}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
mock_create_bookmark.assert_called_with(
ANY, "", self.get_or_create_test_user(), disable_html_snapshot=False
@@ -479,7 +479,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
) as mock_create_bookmark:
data = {"url": "https://example.com/"}
self.post(
reverse("bookmarks:bookmark-list") + "?disable_html_snapshot",
reverse("linkding:bookmark-list") + "?disable_html_snapshot",
data,
status.HTTP_201_CREATED,
)
@@ -502,7 +502,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
"is_archived": True,
"tag_names": ["tag1", "tag2"],
}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertEqual(bookmark.id, original_bookmark.id)
self.assertEqual(bookmark.url, data["url"])
@@ -526,7 +526,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
"description": "Test description",
"tag_names": ["tag 1", "tag 2"],
}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
tag_names = [tag.name for tag in bookmark.tags.all()]
self.assertListEqual(tag_names, ["tag-1", "tag-2"])
@@ -536,7 +536,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
data = {"url": "https://example.com/"}
self.post(
reverse("bookmarks:bookmark-list") + "?disable_scraping",
reverse("linkding:bookmark-list") + "?disable_scraping",
data,
status.HTTP_201_CREATED,
)
@@ -561,7 +561,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
"is_archived": True,
"tag_names": ["tag1", "tag2"],
}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertEqual(bookmark.url, data["url"])
self.assertEqual(bookmark.title, data["title"])
@@ -575,7 +575,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
data = {"url": "https://example.com/"}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertFalse(bookmark.is_archived)
@@ -583,7 +583,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
data = {"url": "https://example.com/", "unread": True}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertTrue(bookmark.unread)
@@ -591,7 +591,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
data = {"url": "https://example.com/"}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertFalse(bookmark.unread)
@@ -599,7 +599,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
data = {"url": "https://example.com/", "shared": True}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertTrue(bookmark.shared)
@@ -607,7 +607,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
data = {"url": "https://example.com/"}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertFalse(bookmark.shared)
@@ -621,7 +621,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
profile.save()
data = {"url": "https://example.com/", "tag_names": [tag1.name]}
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data["url"])
self.assertCountEqual(bookmark.tags.all(), [tag1, tag2])
@@ -629,7 +629,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
bookmark = self.setup_bookmark()
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual([response.data], [bookmark])
@@ -641,7 +641,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
tags=[tag1],
)
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual([response.data], [bookmark])
@@ -655,7 +655,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
),
)
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertEqual(
response.data["web_archive_snapshot_url"],
@@ -667,7 +667,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
data = {"url": "https://example.com/updated"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertEqual(updated_bookmark.url, data["url"])
@@ -682,7 +682,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
"website_title": "test",
"website_description": "test",
}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertEqual(data["url"], updated_bookmark.url)
@@ -699,7 +699,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
data = {"title": "https://example.com/"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)
def test_update_bookmark_with_minimal_payload_does_not_modify_bookmark(self):
@@ -709,7 +709,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
)
data = {"url": "https://example.com/"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertEqual(updated_bookmark.url, data["url"])
@@ -726,7 +726,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
data = {"url": "https://example.com/", "unread": True}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertEqual(updated_bookmark.unread, True)
@@ -736,7 +736,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
data = {"url": "https://example.com/", "shared": True}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertEqual(updated_bookmark.shared, True)
@@ -752,7 +752,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
profile.save()
data = {"url": "https://example.com/", "tag_names": [tag1.name]}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertCountEqual(updated_bookmark.tags.all(), [tag1, tag2])
@@ -767,17 +767,17 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
# if the URL isn't modified it's not a duplicate
data = {"url": edited_bookmark.url}
url = reverse("bookmarks:bookmark-detail", args=[edited_bookmark.id])
url = reverse("linkding:bookmark-detail", args=[edited_bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
# if the URL is already bookmarked by another user, it's not a duplicate
data = {"url": other_user_bookmark.url}
url = reverse("bookmarks:bookmark-detail", args=[edited_bookmark.id])
url = reverse("linkding:bookmark-detail", args=[edited_bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK)
# if the URL is already bookmarked by the same user, it's a duplicate
data = {"url": existing_bookmark.url}
url = reverse("bookmarks:bookmark-detail", args=[edited_bookmark.id])
url = reverse("linkding:bookmark-detail", args=[edited_bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)
def test_patch_bookmark(self):
@@ -785,55 +785,55 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark()
data = {"url": "https://example.com"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
self.assertEqual(bookmark.url, data["url"])
data = {"title": "Updated title"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
self.assertEqual(bookmark.title, data["title"])
data = {"description": "Updated description"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
self.assertEqual(bookmark.description, data["description"])
data = {"notes": "Updated notes"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
self.assertEqual(bookmark.notes, data["notes"])
data = {"unread": True}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
self.assertTrue(bookmark.unread)
data = {"unread": False}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
self.assertFalse(bookmark.unread)
data = {"shared": True}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
self.assertTrue(bookmark.shared)
data = {"shared": False}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
self.assertFalse(bookmark.shared)
data = {"tag_names": ["updated-tag-1", "updated-tag-2"]}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
bookmark.refresh_from_db()
tag_names = [tag.name for tag in bookmark.tags.all()]
@@ -848,7 +848,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
"website_title": "test",
"website_description": "test",
}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertNotEqual(
@@ -865,7 +865,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
is_archived=True, unread=True, shared=True, tags=[self.setup_tag()]
)
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, {}, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertEqual(updated_bookmark.url, bookmark.url)
@@ -888,7 +888,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
profile.save()
data = {"tag_names": [tag1.name]}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertCountEqual(updated_bookmark.tags.all(), [tag1, tag2])
@@ -897,7 +897,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
bookmark = self.setup_bookmark()
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
self.assertEqual(len(Bookmark.objects.filter(id=bookmark.id)), 0)
@@ -905,7 +905,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
bookmark = self.setup_bookmark()
url = reverse("bookmarks:bookmark-archive", args=[bookmark.id])
url = reverse("linkding:bookmark-archive", args=[bookmark.id])
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertTrue(bookmark.is_archived)
@@ -914,7 +914,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
bookmark = self.setup_bookmark(is_archived=True)
url = reverse("bookmarks:bookmark-unarchive", args=[bookmark.id])
url = reverse("linkding:bookmark-unarchive", args=[bookmark.id])
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
bookmark = Bookmark.objects.get(id=bookmark.id)
self.assertFalse(bookmark.is_archived)
@@ -922,7 +922,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def test_check_returns_no_bookmark_if_url_is_not_bookmarked(self):
self.authenticate()
url = reverse("bookmarks:bookmark-check")
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus("https://example.com")
response = self.get(
f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK
@@ -945,7 +945,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
)
mock_load_website_metadata.return_value = expected_metadata
url = reverse("bookmarks:bookmark-check")
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus("https://example.com")
response = self.get(
f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK
@@ -969,7 +969,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
preview_image_file="preview.png",
)
url = reverse("bookmarks:bookmark-check")
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus("https://example.com")
response = self.get(
f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK
@@ -1006,7 +1006,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
)
mock_load_website_metadata.return_value = expected_metadata
url = reverse("bookmarks:bookmark-check")
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus("https://example.com")
response = self.get(
f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK
@@ -1022,7 +1022,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def test_check_returns_no_auto_tags_if_none_configured(self):
self.authenticate()
url = reverse("bookmarks:bookmark-check")
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus("https://example.com")
response = self.get(
f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK
@@ -1038,7 +1038,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
profile.auto_tagging_rules = "example.com tag1 tag2"
profile.save()
url = reverse("bookmarks:bookmark-check")
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus("https://example.com")
response = self.get(
f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK
@@ -1047,6 +1047,43 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertCountEqual(auto_tags, ["tag1", "tag2"])
def test_check_ignore_cache(self):
self.authenticate()
with patch.object(
website_loader, "load_website_metadata"
) as mock_load_website_metadata:
expected_metadata = WebsiteMetadata(
"https://example.com",
"Scraped metadata",
"Scraped description",
"https://example.com/preview.png",
)
mock_load_website_metadata.return_value = expected_metadata
# Does not ignore cache by default
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus("https://example.com")
self.get(
f"{url}?url={check_url}",
expected_status_code=status.HTTP_200_OK,
)
mock_load_website_metadata.assert_called_once_with(
"https://example.com", ignore_cache=False
)
mock_load_website_metadata.reset_mock()
# Ignores cache based on query param
self.get(
f"{url}?url={check_url}&ignore_cache=true",
expected_status_code=status.HTTP_200_OK,
)
mock_load_website_metadata.assert_called_once_with(
"https://example.com", ignore_cache=True
)
def test_can_only_access_own_bookmarks(self):
self.authenticate()
self.setup_bookmark()
@@ -1059,23 +1096,23 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
inaccessible_shared_bookmark = self.setup_bookmark(user=other_user, shared=True)
self.setup_bookmark(user=other_user, is_archived=True)
url = reverse("bookmarks:bookmark-list")
url = reverse("linkding:bookmark-list")
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 1)
url = reverse("bookmarks:bookmark-archived")
url = reverse("linkding:bookmark-archived")
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 1)
url = reverse("bookmarks:bookmark-detail", args=[inaccessible_bookmark.id])
url = reverse("linkding:bookmark-detail", args=[inaccessible_bookmark.id])
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse(
"bookmarks:bookmark-detail", args=[inaccessible_shared_bookmark.id]
"linkding:bookmark-detail", args=[inaccessible_shared_bookmark.id]
)
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse("bookmarks:bookmark-detail", args=[inaccessible_bookmark.id])
url = reverse("linkding:bookmark-detail", args=[inaccessible_bookmark.id])
self.put(
url,
{url: "https://example.com/"},
@@ -1084,7 +1121,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.patch(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse(
"bookmarks:bookmark-detail", args=[inaccessible_shared_bookmark.id]
"linkding:bookmark-detail", args=[inaccessible_shared_bookmark.id]
)
self.put(
url,
@@ -1093,31 +1130,31 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
)
self.patch(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse("bookmarks:bookmark-detail", args=[inaccessible_bookmark.id])
url = reverse("linkding:bookmark-detail", args=[inaccessible_bookmark.id])
self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse(
"bookmarks:bookmark-detail", args=[inaccessible_shared_bookmark.id]
"linkding:bookmark-detail", args=[inaccessible_shared_bookmark.id]
)
self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse("bookmarks:bookmark-archive", args=[inaccessible_bookmark.id])
url = reverse("linkding:bookmark-archive", args=[inaccessible_bookmark.id])
self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse(
"bookmarks:bookmark-archive", args=[inaccessible_shared_bookmark.id]
"linkding:bookmark-archive", args=[inaccessible_shared_bookmark.id]
)
self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse("bookmarks:bookmark-unarchive", args=[inaccessible_bookmark.id])
url = reverse("linkding:bookmark-unarchive", args=[inaccessible_bookmark.id])
self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse(
"bookmarks:bookmark-unarchive", args=[inaccessible_shared_bookmark.id]
"linkding:bookmark-unarchive", args=[inaccessible_shared_bookmark.id]
)
self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)
url = reverse("bookmarks:bookmark-check")
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus(inaccessible_bookmark.url)
response = self.get(
f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK
@@ -1153,7 +1190,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
# default profile
profile = self.user.profile
url = reverse("bookmarks:user-profile")
url = reverse("linkding:user-profile")
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertUserProfile(response, profile)
@@ -1176,7 +1213,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
}
profile.save()
url = reverse("bookmarks:user-profile")
url = reverse("linkding:user-profile")
response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertUserProfile(response, profile)
@@ -1194,7 +1231,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
response = self.client.post(
reverse("bookmarks:bookmark-singlefile"),
reverse("linkding:bookmark-singlefile"),
self.create_singlefile_upload_body(),
format="multipart",
expected_status_code=status.HTTP_201_CREATED,
@@ -1211,7 +1248,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
self.client.post(
reverse("bookmarks:bookmark-singlefile"),
reverse("linkding:bookmark-singlefile"),
self.create_singlefile_upload_body(),
format="multipart",
expected_status_code=status.HTTP_201_CREATED,
@@ -1232,7 +1269,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.authenticate()
self.client.post(
reverse("bookmarks:bookmark-singlefile"),
reverse("linkding:bookmark-singlefile"),
self.create_singlefile_upload_body(),
format="multipart",
expected_status_code=status.HTTP_201_CREATED,
@@ -1248,7 +1285,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
) as mock_create_bookmark:
self.authenticate()
self.client.post(
reverse("bookmarks:bookmark-singlefile"),
reverse("linkding:bookmark-singlefile"),
self.create_singlefile_upload_body(),
format="multipart",
expected_status_code=status.HTTP_201_CREATED,
@@ -1267,7 +1304,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
file = io.BytesIO(file_content)
file.name = "snapshot.html"
response = self.client.post(
reverse("bookmarks:bookmark-singlefile"),
reverse("linkding:bookmark-singlefile"),
{"file": file},
format="multipart",
expected_status_code=status.HTTP_400_BAD_REQUEST,
@@ -1278,7 +1315,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
# Missing 'file'
response = self.client.post(
reverse("bookmarks:bookmark-singlefile"),
reverse("linkding:bookmark-singlefile"),
{"url": "https://example.com"},
format="multipart",
expected_status_code=status.HTTP_400_BAD_REQUEST,
@@ -1291,7 +1328,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def test_singlefile_upload_disabled(self):
self.authenticate()
self.client.post(
reverse("bookmarks:bookmark-singlefile"),
reverse("linkding:bookmark-singlefile"),
self.create_singlefile_upload_body(),
format="multipart",
expected_status_code=status.HTTP_403_FORBIDDEN,

View File

@@ -33,7 +33,7 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
context = CaptureQueriesContext(self.get_connection())
with context:
self.get(
reverse("bookmarks:bookmark-list"),
reverse("linkding:bookmark-list"),
expected_status_code=status.HTTP_200_OK,
)
@@ -51,7 +51,7 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
context = CaptureQueriesContext(self.get_connection())
with context:
self.get(
reverse("bookmarks:bookmark-archived"),
reverse("linkding:bookmark-archived"),
expected_status_code=status.HTTP_200_OK,
)
@@ -70,7 +70,7 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
context = CaptureQueriesContext(self.get_connection())
with context:
self.get(
reverse("bookmarks:bookmark-shared"),
reverse("linkding:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)

View File

@@ -16,36 +16,36 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_list_bookmarks_requires_authentication(self):
self.get(
reverse("bookmarks:bookmark-list"),
reverse("linkding:bookmark-list"),
expected_status_code=status.HTTP_401_UNAUTHORIZED,
)
self.authenticate()
self.get(
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
reverse("linkding:bookmark-list"), expected_status_code=status.HTTP_200_OK
)
def test_list_archived_bookmarks_requires_authentication(self):
self.get(
reverse("bookmarks:bookmark-archived"),
reverse("linkding:bookmark-archived"),
expected_status_code=status.HTTP_401_UNAUTHORIZED,
)
self.authenticate()
self.get(
reverse("bookmarks:bookmark-archived"),
reverse("linkding:bookmark-archived"),
expected_status_code=status.HTTP_200_OK,
)
def test_list_shared_bookmarks_does_not_require_authentication(self):
self.get(
reverse("bookmarks:bookmark-shared"),
reverse("linkding:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)
self.authenticate()
self.get(
reverse("bookmarks:bookmark-shared"),
reverse("linkding:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)
@@ -61,16 +61,14 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
"tag_names": ["tag1", "tag2"],
}
self.post(
reverse("bookmarks:bookmark-list"), data, status.HTTP_401_UNAUTHORIZED
)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_401_UNAUTHORIZED)
self.authenticate()
self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
self.post(reverse("linkding:bookmark-list"), data, status.HTTP_201_CREATED)
def test_get_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark()
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -80,7 +78,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_update_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark()
data = {"url": "https://example.com/"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -93,14 +91,14 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
other_user = self.setup_user()
bookmark = self.setup_bookmark(user=other_user)
data = {"url": "https://example.com/"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_404_NOT_FOUND)
def test_patch_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark()
data = {"url": "https://example.com"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -113,13 +111,13 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
other_user = self.setup_user()
bookmark = self.setup_bookmark(user=other_user)
data = {"url": "https://example.com"}
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_404_NOT_FOUND)
def test_delete_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark()
url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
url = reverse("linkding:bookmark-detail", args=[bookmark.id])
self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -128,7 +126,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_archive_requires_authentication(self):
bookmark = self.setup_bookmark()
url = reverse("bookmarks:bookmark-archive", args=[bookmark.id])
url = reverse("linkding:bookmark-archive", args=[bookmark.id])
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -137,7 +135,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_unarchive_requires_authentication(self):
bookmark = self.setup_bookmark(is_archived=True)
url = reverse("bookmarks:bookmark-unarchive", args=[bookmark.id])
url = reverse("linkding:bookmark-unarchive", args=[bookmark.id])
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -145,7 +143,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
def test_check_requires_authentication(self):
url = reverse("bookmarks:bookmark-check")
url = reverse("linkding:bookmark-check")
check_url = urllib.parse.quote_plus("https://example.com")
self.get(
@@ -156,7 +154,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
self.get(f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK)
def test_user_profile_requires_authentication(self):
url = reverse("bookmarks:user-profile")
url = reverse("linkding:user-profile")
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@@ -164,6 +162,6 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
self.get(url, expected_status_code=status.HTTP_200_OK)
def test_singlefile_upload_requires_authentication(self):
url = reverse("bookmarks:bookmark-singlefile")
url = reverse("linkding:bookmark-singlefile")
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)

View File

@@ -43,7 +43,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertInHTML(
f"""
<a href="{url}"
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
title="View snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
{label_content}
</a>
<span>|</span>
@@ -65,7 +65,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
count=1,
):
if base_url is None:
base_url = reverse("bookmarks:index")
base_url = reverse("linkding:bookmarks.index")
details_url = base_url + f"?details={bookmark.id}"
self.assertInHTML(
f"""
@@ -76,7 +76,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
)
def assertEditLinkCount(self, html: str, bookmark: Bookmark, count=1):
edit_url = reverse("bookmarks:edit", args=[bookmark.id])
edit_url = reverse("linkding:bookmarks.edit", args=[bookmark.id])
self.assertInHTML(
f"""
<a href="{edit_url}?return_url=/bookmarks">Edit</a>
@@ -559,6 +559,31 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_self"
)
def test_should_render_latest_snapshot_link_if_one_exists(self):
bookmark = self.setup_date_format_test(
UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE
)
bookmark.latest_snapshot = self.setup_asset(bookmark)
bookmark.save()
html = self.render_template()
formatted_date = formats.date_format(bookmark.date_added, "SHORT_DATE_FORMAT")
snapshot_url = reverse(
"linkding:assets.view", args=[bookmark.latest_snapshot.id]
)
# Check that the snapshot link is rendered with the correct URL and title
self.assertInHTML(
f"""
<a href="{snapshot_url}"
title="View latest snapshot" target="_blank" rel="noopener">
{formatted_date}
</a>
<span>|</span>
""",
html,
)
def test_should_reflect_unread_state_as_css_class(self):
self.setup_bookmark(unread=True)
html = self.render_template()
@@ -660,7 +685,9 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
bookmark = self.setup_bookmark(user=other_user, shared=True)
html = self.render_template(context_type=contexts.SharedBookmarkListContext)
self.assertViewLink(html, bookmark, base_url=reverse("bookmarks:shared"))
self.assertViewLink(
html, bookmark, base_url=reverse("linkding:bookmarks.shared")
)
self.assertNoBookmarkActions(html, bookmark)
self.assertShareInfo(html, bookmark)
@@ -857,6 +884,21 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
)
self.assertNotes(html, note_html, 1)
def test_note_renders_markdown_with_linkify(self):
# Should linkify plain URL
self.setup_bookmark(notes="Example: https://example.com")
html = self.render_template()
note_html = '<p>Example: <a href="https://example.com" rel="nofollow">https://example.com</a></p>'
self.assertNotes(html, note_html, 1)
# Should not linkify URL in markdown link
self.setup_bookmark(notes="[https://example.com](https://example.com)")
html = self.render_template()
note_html = '<p><a href="https://example.com" rel="nofollow">https://example.com</a></p>'
self.assertNotes(html, note_html, 1)
def test_note_cleans_html(self):
self.setup_bookmark(notes='<script>alert("test")</script>')
self.setup_bookmark(
@@ -952,7 +994,9 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertWebArchiveLink(
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_blank"
)
self.assertViewLink(html, bookmark, base_url=reverse("bookmarks:shared"))
self.assertViewLink(
html, bookmark, base_url=reverse("linkding:bookmarks.shared")
)
self.assertNoBookmarkActions(html, bookmark)
self.assertShareInfo(html, bookmark)
self.assertMarkAsReadButton(html, bookmark, count=0)

View File

@@ -1,6 +1,5 @@
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone
@@ -22,17 +21,31 @@ from bookmarks.services.bookmarks import (
share_bookmarks,
unshare_bookmarks,
enhance_with_website_metadata,
refresh_bookmarks_metadata,
)
from bookmarks.tests.helpers import BookmarkFactoryMixin
User = get_user_model()
class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self) -> None:
self.get_or_create_test_user()
self.mock_schedule_refresh_metadata_patcher = patch(
"bookmarks.services.bookmarks.tasks.refresh_metadata"
)
self.mock_schedule_refresh_metadata = (
self.mock_schedule_refresh_metadata_patcher.start()
)
self.mock_load_preview_image_patcher = patch(
"bookmarks.services.bookmarks.tasks.load_preview_image"
)
self.mock_load_preview_image = self.mock_load_preview_image_patcher.start()
def tearDown(self):
self.mock_schedule_refresh_metadata_patcher.stop()
self.mock_load_preview_image_patcher.stop()
def test_create_should_not_update_website_metadata(self):
with patch.object(
website_loader, "load_website_metadata"
@@ -270,9 +283,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
def test_archive_bookmarks_should_only_archive_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark()
inaccessible_bookmark = self.setup_bookmark(user=other_user)
@@ -327,9 +338,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
def test_unarchive_bookmarks_should_only_unarchive_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
bookmark1 = self.setup_bookmark(is_archived=True)
bookmark2 = self.setup_bookmark(is_archived=True)
inaccessible_bookmark = self.setup_bookmark(is_archived=True, user=other_user)
@@ -382,9 +391,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
def test_delete_bookmarks_should_only_delete_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark()
inaccessible_bookmark = self.setup_bookmark(user=other_user)
@@ -508,9 +515,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
def test_tag_bookmarks_should_only_tag_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark()
inaccessible_bookmark = self.setup_bookmark(user=other_user)
@@ -591,9 +596,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertCountEqual(bookmark3.tags.all(), [])
def test_untag_bookmarks_should_only_tag_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
tag1 = self.setup_tag()
tag2 = self.setup_tag()
bookmark1 = self.setup_bookmark(tags=[tag1, tag2])
@@ -658,9 +661,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
def test_mark_bookmarks_as_read_should_only_update_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
bookmark1 = self.setup_bookmark(unread=True)
bookmark2 = self.setup_bookmark(unread=True)
inaccessible_bookmark = self.setup_bookmark(unread=True, user=other_user)
@@ -715,9 +716,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
def test_mark_bookmarks_as_unread_should_only_update_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
bookmark1 = self.setup_bookmark(unread=False)
bookmark2 = self.setup_bookmark(unread=False)
inaccessible_bookmark = self.setup_bookmark(unread=False, user=other_user)
@@ -770,9 +769,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared)
def test_share_bookmarks_should_only_update_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
bookmark1 = self.setup_bookmark(shared=False)
bookmark2 = self.setup_bookmark(shared=False)
inaccessible_bookmark = self.setup_bookmark(shared=False, user=other_user)
@@ -825,9 +822,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared)
def test_unshare_bookmarks_should_only_update_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
bookmark1 = self.setup_bookmark(shared=True)
bookmark2 = self.setup_bookmark(shared=True)
inaccessible_bookmark = self.setup_bookmark(shared=True, user=other_user)
@@ -912,3 +907,70 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual("", bookmark.title)
self.assertEqual("", bookmark.description)
def test_refresh_bookmarks_metadata(self):
bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark()
refresh_bookmarks_metadata(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 3)
self.assertEqual(self.mock_load_preview_image.call_count, 3)
def test_refresh_bookmarks_metadata_should_only_refresh_specified_bookmarks(self):
bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark()
refresh_bookmarks_metadata(
[bookmark1.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 2)
self.assertEqual(self.mock_load_preview_image.call_count, 2)
for call_args in self.mock_schedule_refresh_metadata.call_args_list:
args, kwargs = call_args
self.assertNotIn(bookmark2.id, args)
for call_args in self.mock_load_preview_image.call_args_list:
args, kwargs = call_args
self.assertNotIn(bookmark2.id, args)
def test_refresh_bookmarks_metadata_should_only_refresh_user_owned_bookmarks(self):
other_user = self.setup_user()
bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark()
inaccessible_bookmark = self.setup_bookmark(user=other_user)
refresh_bookmarks_metadata(
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
self.get_or_create_test_user(),
)
self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 2)
self.assertEqual(self.mock_load_preview_image.call_count, 2)
for call_args in self.mock_schedule_refresh_metadata.call_args_list:
args, kwargs = call_args
self.assertNotIn(inaccessible_bookmark.id, args)
for call_args in self.mock_load_preview_image.call_args_list:
args, kwargs = call_args
self.assertNotIn(inaccessible_bookmark.id, args)
def test_refresh_bookmarks_metadata_should_accept_mix_of_int_and_string_ids(self):
bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark()
refresh_bookmarks_metadata(
[str(bookmark1.id), str(bookmark2.id), bookmark3.id],
self.get_or_create_test_user(),
)
self.assertEqual(self.mock_schedule_refresh_metadata.call_count, 3)
self.assertEqual(self.mock_load_preview_image.call_count, 3)

View File

@@ -8,6 +8,7 @@ from waybackpy.exceptions import WaybackError
from bookmarks.models import BookmarkAsset, UserProfile
from bookmarks.services import tasks
from bookmarks.services.website_loader import WebsiteMetadata
from bookmarks.tests.helpers import BookmarkFactoryMixin
@@ -615,3 +616,52 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(count, 3)
self.assertEqual(BookmarkAsset.objects.count(), count)
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
def test_refresh_metadata_task_not_called_when_background_tasks_disabled(self):
bookmark = self.setup_bookmark()
with mock.patch(
"bookmarks.services.tasks._refresh_metadata_task"
) as mock_refresh_metadata_task:
tasks.refresh_metadata(bookmark)
mock_refresh_metadata_task.assert_not_called()
@override_settings(LD_DISABLE_BACKGROUND_TASKS=False)
def test_refresh_metadata_task_called_when_background_tasks_enabled(self):
bookmark = self.setup_bookmark()
with mock.patch(
"bookmarks.services.tasks._refresh_metadata_task"
) as mock_refresh_metadata_task:
tasks.refresh_metadata(bookmark)
mock_refresh_metadata_task.assert_called_once()
def test_refresh_metadata_task_should_handle_missing_bookmark(self):
with mock.patch(
"bookmarks.services.website_loader.load_website_metadata"
) as mock_load_website_metadata:
tasks._refresh_metadata_task(123)
mock_load_website_metadata.assert_not_called()
def test_refresh_metadata_updates_title_description(self):
bookmark = self.setup_bookmark(
title="Initial title",
description="Initial description",
)
mock_website_metadata = WebsiteMetadata(
url=bookmark.url,
title="New title",
description="New description",
preview_image=None,
)
with mock.patch(
"bookmarks.services.tasks.load_website_metadata"
) as mock_load_website_metadata:
mock_load_website_metadata.return_value = mock_website_metadata
tasks.refresh_metadata(bookmark)
bookmark.refresh_from_db()
self.assertEqual(bookmark.title, "New title")
self.assertEqual(bookmark.description, "New description")

View File

@@ -12,21 +12,21 @@ class MockUrlConf:
class ContextPathTestCase(TestCase):
def setUp(self):
self.siteroot_urls = importlib.import_module("siteroot.urls")
self.urls_module = importlib.import_module("bookmarks.urls")
@override_settings(LD_CONTEXT_PATH=None)
def tearDown(self):
importlib.reload(self.siteroot_urls)
importlib.reload(self.urls_module)
@override_settings(LD_CONTEXT_PATH="linkding/")
def test_route_with_context_path(self):
module = importlib.reload(self.siteroot_urls)
module = importlib.reload(self.urls_module)
# pass mock config instead of actual module to prevent caching the
# url config in django.urls.reverse
urlconf = MockUrlConf(module)
test_cases = [
("bookmarks:index", "/linkding/bookmarks"),
("bookmarks:bookmark-list", "/linkding/api/bookmarks/"),
("linkding:bookmarks.index", "/linkding/bookmarks"),
("linkding:bookmark-list", "/linkding/api/bookmarks/"),
("login", "/linkding/login/"),
(
"admin:bookmarks_bookmark_changelist",
@@ -40,13 +40,13 @@ class ContextPathTestCase(TestCase):
@override_settings(LD_CONTEXT_PATH="")
def test_route_without_context_path(self):
module = importlib.reload(self.siteroot_urls)
module = importlib.reload(self.urls_module)
# pass mock config instead of actual module to prevent caching the
# url config in django.urls.reverse
urlconf = MockUrlConf(module)
test_cases = [
("bookmarks:index", "/bookmarks"),
("bookmarks:bookmark-list", "/api/bookmarks/"),
("linkding:bookmarks.index", "/bookmarks"),
("linkding:bookmark-list", "/api/bookmarks/"),
("login", "/login/"),
("admin:bookmarks_bookmark_changelist", "/admin/bookmarks/bookmark/"),
]

View File

@@ -10,7 +10,7 @@ class CustomCssViewTestCase(TestCase, BookmarkFactoryMixin):
self.client.force_login(user)
def test_with_empty_css(self):
response = self.client.get(reverse("bookmarks:custom_css"))
response = self.client.get(reverse("linkding:custom_css"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "text/css")
self.assertEqual(response.headers["Cache-Control"], "public, max-age=2592000")
@@ -21,7 +21,7 @@ class CustomCssViewTestCase(TestCase, BookmarkFactoryMixin):
self.user.profile.custom_css = css
self.user.profile.save()
response = self.client.get(reverse("bookmarks:custom_css"))
response = self.client.get(reverse("linkding:custom_css"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "text/css")
self.assertEqual(response.headers["Cache-Control"], "public, max-age=2592000")

View File

@@ -86,8 +86,8 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
"<DD>Example description[linkding-notes]Example notes[/linkding-notes]",
'<DT><A HREF="https://example.com/6" ADD_DATE="6" LAST_MODIFIED="66" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>',
"<DD>[linkding-notes]Example notes[/linkding-notes]",
'<DT><A HREF="https://example.com/7" ADD_DATE="7" LAST_MODIFIED="77" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>',
'<DT><A HREF="https://example.com/8" ADD_DATE="8" LAST_MODIFIED="88" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>',
'<DT><A HREF="https://example.com/7" ADD_DATE="7" LAST_MODIFIED="77" PRIVATE="1" TOREAD="0" TAGS="linkding:bookmarks.archived">Title 7</A>',
'<DT><A HREF="https://example.com/8" ADD_DATE="8" LAST_MODIFIED="88" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:bookmarks.archived">Title 8</A>',
]
self.assertIn("\n\r".join(lines), html)

View File

@@ -25,7 +25,7 @@ class ExporterPerformanceTestCase(TestCase, BookmarkFactoryMixin):
# capture number of queries
context = CaptureQueriesContext(self.get_connection())
with context:
self.client.get(reverse("bookmarks:settings.export"), follow=True)
self.client.get(reverse("linkding:settings.export"), follow=True)
number_of_queries = context.final_queries

View File

@@ -51,12 +51,12 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.assertContains(response, expected_item, count=1)
def test_all_returns_404_for_unknown_feed_token(self):
response = self.client.get(reverse("bookmarks:feeds.all", args=["foo"]))
response = self.client.get(reverse("linkding:feeds.all", args=["foo"]))
self.assertEqual(response.status_code, 404)
def test_all_metadata(self):
feed_url = reverse("bookmarks:feeds.all", args=[self.token.key])
feed_url = reverse("linkding:feeds.all", args=[self.token.key])
response = self.client.get(feed_url)
self.assertEqual(response.status_code, 200)
@@ -77,9 +77,7 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(is_archived=True)
self.setup_bookmark(is_archived=True)
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key])
)
response = self.client.get(reverse("linkding:feeds.all", args=[self.token.key]))
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, bookmarks)
@@ -91,20 +89,18 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(unread=True, user=other_user)
self.setup_bookmark(unread=True, user=other_user)
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key])
)
response = self.client.get(reverse("linkding:feeds.all", args=[self.token.key]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=0)
def test_unread_returns_404_for_unknown_feed_token(self):
response = self.client.get(reverse("bookmarks:feeds.unread", args=["foo"]))
response = self.client.get(reverse("linkding:feeds.unread", args=["foo"]))
self.assertEqual(response.status_code, 404)
def test_unread_metadata(self):
feed_url = reverse("bookmarks:feeds.unread", args=[self.token.key])
feed_url = reverse("linkding:feeds.unread", args=[self.token.key])
response = self.client.get(feed_url)
self.assertEqual(response.status_code, 200)
@@ -130,7 +126,7 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
]
response = self.client.get(
reverse("bookmarks:feeds.unread", args=[self.token.key])
reverse("linkding:feeds.unread", args=[self.token.key])
)
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, unread_bookmarks)
@@ -144,19 +140,19 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(unread=True, user=other_user)
response = self.client.get(
reverse("bookmarks:feeds.unread", args=[self.token.key])
reverse("linkding:feeds.unread", args=[self.token.key])
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=0)
def test_shared_returns_404_for_unknown_feed_token(self):
response = self.client.get(reverse("bookmarks:feeds.shared", args=["foo"]))
response = self.client.get(reverse("linkding:feeds.shared", args=["foo"]))
self.assertEqual(response.status_code, 404)
def test_shared_metadata(self):
feed_url = reverse("bookmarks:feeds.shared", args=[self.token.key])
feed_url = reverse("linkding:feeds.shared", args=[self.token.key])
response = self.client.get(feed_url)
self.assertEqual(response.status_code, 200)
@@ -182,18 +178,18 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
]
response = self.client.get(
reverse("bookmarks:feeds.shared", args=[self.token.key])
reverse("linkding:feeds.shared", args=[self.token.key])
)
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, shared_bookmarks)
def test_public_shared_does_not_require_auth(self):
response = self.client.get(reverse("bookmarks:feeds.public_shared"))
response = self.client.get(reverse("linkding:feeds.public_shared"))
self.assertEqual(response.status_code, 200)
def test_public_shared_metadata(self):
feed_url = reverse("bookmarks:feeds.public_shared")
feed_url = reverse("linkding:feeds.public_shared")
response = self.client.get(feed_url)
self.assertEqual(response.status_code, 200)
@@ -223,7 +219,7 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(shared=True, user=user1, description="test"),
]
response = self.client.get(reverse("bookmarks:feeds.public_shared"))
response = self.client.get(reverse("linkding:feeds.public_shared"))
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, public_shared_bookmarks)
@@ -237,7 +233,7 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark()
self.setup_bookmark()
feed_url = reverse("bookmarks:feeds.all", args=[self.token.key])
feed_url = reverse("linkding:feeds.all", args=[self.token.key])
url = feed_url + f"?q={bookmark1.title}"
response = self.client.get(url)
@@ -267,22 +263,20 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(unread=False),
# without unread parameter
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key])
)
response = self.client.get(reverse("linkding:feeds.all", args=[self.token.key]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=6)
# with unread=yes
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key]) + "?unread=yes"
reverse("linkding:feeds.all", args=[self.token.key]) + "?unread=yes"
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=2)
# with unread=no
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key]) + "?unread=no"
reverse("linkding:feeds.all", args=[self.token.key]) + "?unread=no"
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=4)
@@ -296,22 +290,20 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(shared=False)
# without shared parameter
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key])
)
response = self.client.get(reverse("linkding:feeds.all", args=[self.token.key]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=6)
# with shared=yes
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key]) + "?shared=yes"
reverse("linkding:feeds.all", args=[self.token.key]) + "?shared=yes"
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=2)
# with shared=no
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key]) + "?shared=no"
reverse("linkding:feeds.all", args=[self.token.key]) + "?shared=no"
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=4)
@@ -325,9 +317,7 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
),
]
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key])
)
response = self.client.get(reverse("linkding:feeds.all", args=[self.token.key]))
self.assertEqual(response.status_code, 200)
self.assertFeedItems(response, bookmarks)
@@ -335,22 +325,20 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_numbered_bookmarks(200)
# without limit - defaults to 100
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key])
)
response = self.client.get(reverse("linkding:feeds.all", args=[self.token.key]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=100)
# with increased limit
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key]) + "?limit=200"
reverse("linkding:feeds.all", args=[self.token.key]) + "?limit=200"
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=200)
# with decreased limit
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key]) + "?limit=5"
reverse("linkding:feeds.all", args=[self.token.key]) + "?limit=5"
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=5)
@@ -359,9 +347,7 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(
title="test\n\r\t\0\x08title", description="test\n\r\t\0\x08description"
)
response = self.client.get(
reverse("bookmarks:feeds.all", args=[self.token.key])
)
response = self.client.get(reverse("linkding:feeds.all", args=[self.token.key]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "<item>", count=1)
self.assertContains(response, f"<title>test\n\r\ttitle</title>", count=1)

View File

@@ -30,7 +30,7 @@ class FeedsPerformanceTestCase(TestCase, BookmarkFactoryMixin):
# capture number of queries
context = CaptureQueriesContext(self.get_connection())
with context:
feed_url = reverse("bookmarks:feeds.all", args=[self.token.key])
feed_url = reverse("linkding:feeds.all", args=[self.token.key])
self.client.get(feed_url)
number_of_queries = context.final_queries

View File

@@ -419,7 +419,7 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
def test_archived_state(self):
test_html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com/1" ADD_DATE="1" TAGS="tag1,tag2,linkding:archived">Example title 1</A>
<DT><A HREF="https://example.com/1" ADD_DATE="1" TAGS="tag1,tag2,linkding:bookmarks.archived">Example title 1</A>
<DD>Example description 1</DD>
<DT><A HREF="https://example.com/2" ADD_DATE="1" PRIVATE="1" TAGS="tag1,tag2">Example title 2</A>
<DD>Example description 2</DD>

View File

@@ -14,12 +14,19 @@ class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def test_nav_menu_should_respect_share_profile_setting(self):
self.user.profile.enable_sharing = False
self.user.profile.save()
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode()
self.assertInHTML(
f"""
<a href="{reverse('bookmarks:shared')}" class="menu-link">Shared</a>
<a href="{reverse('linkding:bookmarks.shared')}" class="menu-link">Shared</a>
""",
html,
count=0,
)
self.assertInHTML(
f"""
<a href="{reverse('linkding:bookmarks.shared')}" class="menu-link">Shared bookmarks</a>
""",
html,
count=0,
@@ -27,15 +34,22 @@ class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.user.profile.enable_sharing = True
self.user.profile.save()
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode()
self.assertInHTML(
f"""
<a href="{reverse('bookmarks:shared')}" class="menu-link">Shared</a>
<a href="{reverse('linkding:bookmarks.shared')}" class="menu-link">Shared</a>
""",
html,
count=2,
count=1,
)
self.assertInHTML(
f"""
<a href="{reverse('linkding:bookmarks.shared')}" class="menu-link">Shared bookmarks</a>
""",
html,
count=1,
)
def test_metadata_should_respect_prefetch_links_setting(self):
@@ -43,7 +57,7 @@ class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
settings.enable_link_prefetch = False
settings.save()
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode()
self.assertInHTML(
@@ -55,7 +69,7 @@ class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
settings.enable_link_prefetch = True
settings.save()
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode()
self.assertInHTML(
@@ -65,7 +79,7 @@ class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
)
def test_does_not_link_custom_css_when_empty(self):
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode()
soup = self.make_soup(html)
@@ -77,7 +91,7 @@ class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
profile.custom_css = "body { background-color: red; }"
profile.save()
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode()
soup = self.make_soup(html)
@@ -89,12 +103,12 @@ class LayoutTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
profile.custom_css = "body { background-color: red; }"
profile.save()
response = self.client.get(reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:bookmarks.index"))
html = response.content.decode()
soup = self.make_soup(html)
link = soup.select_one("link[rel='stylesheet'][href*='custom_css']")
expected_url = (
reverse("bookmarks:custom_css") + f"?hash={profile.custom_css_hash}"
reverse("linkding:custom_css") + f"?hash={profile.custom_css_hash}"
)
self.assertEqual(link["href"], expected_url)

View File

@@ -2,7 +2,7 @@ from django.test import TestCase, override_settings
from django.urls import path, include
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
from siteroot.urls import urlpatterns as base_patterns
from bookmarks.urls import urlpatterns as base_patterns
# Register OIDC urls for this test, otherwise login template can not render when OIDC is enabled
urlpatterns = base_patterns + [path("oidc/", include("mozilla_django_oidc.urls"))]
@@ -39,3 +39,6 @@ class LoginViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
oidc_login_link = soup.find("a", string="Login with OIDC")
self.assertIsNotNone(oidc_login_link)
# should have turbo disabled
self.assertEqual("false", oidc_login_link.get("data-turbo"))

View File

@@ -9,28 +9,28 @@ from bookmarks import utils
class OidcSupportTest(TestCase):
def test_should_not_add_oidc_urls_by_default(self):
siteroot_urls = importlib.import_module("siteroot.urls")
importlib.reload(siteroot_urls)
urls_module = importlib.import_module("bookmarks.urls")
importlib.reload(urls_module)
oidc_url_found = any(
isinstance(urlpattern, URLResolver) and urlpattern.pattern._route == "oidc/"
for urlpattern in siteroot_urls.urlpatterns
for urlpattern in urls_module.urlpatterns
)
self.assertFalse(oidc_url_found)
@override_settings(LD_ENABLE_OIDC=True)
def test_should_add_oidc_urls_when_enabled(self):
siteroot_urls = importlib.import_module("siteroot.urls")
importlib.reload(siteroot_urls)
urls_module = importlib.import_module("bookmarks.urls")
importlib.reload(urls_module)
oidc_url_found = any(
isinstance(urlpattern, URLResolver) and urlpattern.pattern._route == "oidc/"
for urlpattern in siteroot_urls.urlpatterns
for urlpattern in urls_module.urlpatterns
)
self.assertTrue(oidc_url_found)
def test_should_not_add_oidc_authentication_backend_by_default(self):
base_settings = importlib.import_module("siteroot.settings.base")
base_settings = importlib.import_module("bookmarks.settings.base")
importlib.reload(base_settings)
self.assertListEqual(
@@ -40,7 +40,7 @@ class OidcSupportTest(TestCase):
def test_should_add_oidc_authentication_backend_when_enabled(self):
os.environ["LD_ENABLE_OIDC"] = "True"
base_settings = importlib.import_module("siteroot.settings.base")
base_settings = importlib.import_module("bookmarks.settings.base")
importlib.reload(base_settings)
self.assertListEqual(
@@ -54,7 +54,7 @@ class OidcSupportTest(TestCase):
def test_default_settings(self):
os.environ["LD_ENABLE_OIDC"] = "True"
base_settings = importlib.import_module("siteroot.settings.base")
base_settings = importlib.import_module("bookmarks.settings.base")
importlib.reload(base_settings)
self.assertEqual(True, base_settings.OIDC_VERIFY_SSL)

View File

@@ -0,0 +1,25 @@
from django.test import TestCase
from django.urls import reverse
class OpenSearchViewTestCase(TestCase):
def test_opensearch_configuration(self):
response = self.client.get(reverse("linkding:opensearch"))
self.assertEqual(response.status_code, 200)
self.assertEqual(
response["content-type"], "application/opensearchdescription+xml"
)
base_url = "http://testserver"
expected_content = f"""
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>Linkding</ShortName>
<Description>Linkding</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">{base_url}/static/favicon.ico</Image>
<Url type="text/html" template="{base_url}/bookmarks?client=opensearch&amp;q={{searchTerms}}"/>
</OpenSearchDescription>
"""
content = response.content.decode()
self.assertXMLEqual(content, expected_content)

View File

@@ -1,7 +1,6 @@
import operator
import datetime
import operator
from django.contrib.auth import get_user_model
from django.db.models import QuerySet
from django.test import TestCase
from django.utils import timezone
@@ -11,8 +10,6 @@ from bookmarks.models import BookmarkSearch, UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, random_sentence
from bookmarks.utils import unique
User = get_user_model()
class QueriesTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self):
@@ -372,9 +369,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.assertQueryResult(query, [[bookmark1, bookmark2]])
def test_query_bookmarks_should_only_return_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
owned_bookmarks = [
self.setup_bookmark(),
self.setup_bookmark(),
@@ -389,9 +384,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.assertQueryResult(query, [owned_bookmarks])
def test_query_archived_bookmarks_should_only_return_user_owned_bookmarks(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
owned_bookmarks = [
self.setup_bookmark(is_archived=True),
self.setup_bookmark(is_archived=True),
@@ -828,9 +821,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.assertQueryResult(query, [[tag]])
def test_query_bookmark_tags_should_only_return_user_owned_tags(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
owned_bookmarks = [
self.setup_bookmark(tags=[self.setup_tag()]),
self.setup_bookmark(tags=[self.setup_tag()]),
@@ -847,9 +838,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
def test_query_archived_bookmark_tags_should_only_return_user_owned_tags(self):
other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user = self.setup_user()
owned_bookmarks = [
self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]),
self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]),

View File

@@ -7,7 +7,7 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class RootViewTestCase(TestCase, BookmarkFactoryMixin):
def test_unauthenticated_user_redirect_to_login_by_default(self):
response = self.client.get(reverse("bookmarks:root"))
response = self.client.get(reverse("linkding:root"))
self.assertRedirects(response, reverse("login"))
def test_unauthenticated_redirect_to_shared_bookmarks_if_configured_in_global_settings(
@@ -17,24 +17,24 @@ class RootViewTestCase(TestCase, BookmarkFactoryMixin):
settings.landing_page = GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS
settings.save()
response = self.client.get(reverse("bookmarks:root"))
self.assertRedirects(response, reverse("bookmarks:shared"))
response = self.client.get(reverse("linkding:root"))
self.assertRedirects(response, reverse("linkding:bookmarks.shared"))
def test_authenticated_user_always_redirected_to_bookmarks(self):
self.client.force_login(self.get_or_create_test_user())
response = self.client.get(reverse("bookmarks:root"))
self.assertRedirects(response, reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:root"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
settings = GlobalSettings.get()
settings.landing_page = GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS
settings.save()
response = self.client.get(reverse("bookmarks:root"))
self.assertRedirects(response, reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:root"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))
settings.landing_page = GlobalSettings.LANDING_PAGE_LOGIN
settings.save()
response = self.client.get(reverse("bookmarks:root"))
self.assertRedirects(response, reverse("bookmarks:index"))
response = self.client.get(reverse("linkding:root"))
self.assertRedirects(response, reverse("linkding:bookmarks.index"))

View File

@@ -25,7 +25,7 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(tags=[self.setup_tag()], is_archived=True)
self.setup_bookmark(tags=[self.setup_tag()], is_archived=True)
response = self.client.get(reverse("bookmarks:settings.export"), follow=True)
response = self.client.get(reverse("linkding:settings.export"), follow=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["content-type"], "text/plain; charset=UTF-8")
@@ -49,7 +49,7 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(tags=[self.setup_tag()], user=other_user),
]
response = self.client.get(reverse("bookmarks:settings.export"), follow=True)
response = self.client.get(reverse("linkding:settings.export"), follow=True)
text = response.content.decode("utf-8")
@@ -61,10 +61,10 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_check_authentication(self):
self.client.logout()
response = self.client.get(reverse("bookmarks:settings.export"), follow=True)
response = self.client.get(reverse("linkding:settings.export"), follow=True)
self.assertRedirects(
response, reverse("login") + "?next=" + reverse("bookmarks:settings.export")
response, reverse("login") + "?next=" + reverse("linkding:settings.export")
)
def test_should_show_hint_when_export_raises_error(self):
@@ -72,9 +72,7 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
"bookmarks.services.exporter.export_netscape_html"
) as mock_export_netscape_html:
mock_export_netscape_html.side_effect = Exception("Nope")
response = self.client.get(
reverse("bookmarks:settings.export"), follow=True
)
response = self.client.get(reverse("linkding:settings.export"), follow=True)
self.assertTemplateUsed(response, "settings/general.html")
self.assertFormErrorHint(

View File

@@ -71,24 +71,24 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
)
def test_should_render_successfully(self):
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
self.assertEqual(response.status_code, 200)
def test_should_check_authentication(self):
self.client.logout()
response = self.client.get(reverse("bookmarks:settings.general"), follow=True)
response = self.client.get(reverse("linkding:settings.general"), follow=True)
self.assertRedirects(
response,
reverse("login") + "?next=" + reverse("bookmarks:settings.general"),
reverse("login") + "?next=" + reverse("linkding:settings.general"),
)
response = self.client.get(reverse("bookmarks:settings.update"), follow=True)
response = self.client.get(reverse("linkding:settings.update"), follow=True)
self.assertRedirects(
response,
reverse("login") + "?next=" + reverse("bookmarks:settings.update"),
reverse("login") + "?next=" + reverse("linkding:settings.update"),
)
def test_update_profile(self):
@@ -121,7 +121,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"collapse_side_panel": True,
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
html = response.content.decode()
@@ -204,7 +204,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
def test_update_profile_with_invalid_form_returns_422(self):
form_data = self.create_profile_form_data({"items_per_page": "-1"})
response = self.client.post(reverse("bookmarks:settings.update"), form_data)
response = self.client.post(reverse("linkding:settings.update"), form_data)
self.assertEqual(response.status_code, 422)
@@ -213,7 +213,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"theme": UserProfile.THEME_DARK,
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
html = response.content.decode()
@@ -229,21 +229,21 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"custom_css": "body { background-color: #000; }",
}
)
self.client.post(reverse("bookmarks:settings.update"), form_data, follow=True)
self.client.post(reverse("linkding:settings.update"), form_data, follow=True)
self.user.profile.refresh_from_db()
expected_hash = hashlib.md5(form_data["custom_css"].encode("utf-8")).hexdigest()
self.assertEqual(expected_hash, self.user.profile.custom_css_hash)
form_data["custom_css"] = "body { background-color: #fff; }"
self.client.post(reverse("bookmarks:settings.update"), form_data, follow=True)
self.client.post(reverse("linkding:settings.update"), form_data, follow=True)
self.user.profile.refresh_from_db()
expected_hash = hashlib.md5(form_data["custom_css"].encode("utf-8")).hexdigest()
self.assertEqual(expected_hash, self.user.profile.custom_css_hash)
form_data["custom_css"] = ""
self.client.post(reverse("bookmarks:settings.update"), form_data, follow=True)
self.client.post(reverse("linkding:settings.update"), form_data, follow=True)
self.user.profile.refresh_from_db()
self.assertEqual("", self.user.profile.custom_css_hash)
@@ -258,14 +258,14 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"enable_favicons": True,
}
)
self.client.post(reverse("bookmarks:settings.update"), form_data)
self.client.post(reverse("linkding:settings.update"), form_data)
mock_schedule_bookmarks_without_favicons.assert_called_once_with(self.user)
# No update scheduled if favicons are already enabled
mock_schedule_bookmarks_without_favicons.reset_mock()
self.client.post(reverse("bookmarks:settings.update"), form_data)
self.client.post(reverse("linkding:settings.update"), form_data)
mock_schedule_bookmarks_without_favicons.assert_not_called()
@@ -276,7 +276,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
}
)
self.client.post(reverse("bookmarks:settings.update"), form_data)
self.client.post(reverse("linkding:settings.update"), form_data)
mock_schedule_bookmarks_without_favicons.assert_not_called()
@@ -288,7 +288,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"refresh_favicons": "",
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
html = response.content.decode()
@@ -302,7 +302,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
tasks, "schedule_refresh_favicons"
) as mock_schedule_refresh_favicons:
form_data = {}
response = self.client.post(reverse("bookmarks:settings.update"), form_data)
response = self.client.post(reverse("linkding:settings.update"), form_data)
html = response.content.decode()
mock_schedule_refresh_favicons.assert_not_called()
@@ -315,7 +315,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
profile.enable_favicons = True
profile.save()
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
html = response.content.decode()
self.assertInHTML(
@@ -333,7 +333,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
profile.enable_favicons = False
profile.save()
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
html = response.content.decode()
self.assertInHTML(
@@ -350,7 +350,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
profile.enable_favicons = True
profile.save()
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
html = response.content.decode()
self.assertInHTML(
@@ -371,14 +371,14 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"enable_preview_images": True,
}
)
self.client.post(reverse("bookmarks:settings.update"), form_data)
self.client.post(reverse("linkding:settings.update"), form_data)
mock_schedule_bookmarks_without_previews.assert_called_once_with(self.user)
# No update scheduled if favicons are already enabled
mock_schedule_bookmarks_without_previews.reset_mock()
self.client.post(reverse("bookmarks:settings.update"), form_data)
self.client.post(reverse("linkding:settings.update"), form_data)
mock_schedule_bookmarks_without_previews.assert_not_called()
@@ -389,14 +389,14 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
}
)
self.client.post(reverse("bookmarks:settings.update"), form_data)
self.client.post(reverse("linkding:settings.update"), form_data)
mock_schedule_bookmarks_without_previews.assert_not_called()
def test_automatic_html_snapshots_should_be_hidden_when_snapshots_not_supported(
self,
):
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
html = response.content.decode()
self.assertInHTML(
@@ -411,7 +411,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
def test_automatic_html_snapshots_should_be_visible_when_snapshots_supported(
self,
):
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
html = response.content.decode()
self.assertInHTML(
@@ -423,7 +423,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
)
def test_about_shows_version_info(self):
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
html = response.content.decode()
self.assertInHTML(
@@ -478,7 +478,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"create_missing_html_snapshots": "",
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
html = response.content.decode()
@@ -498,7 +498,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"create_missing_html_snapshots": "",
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
html = response.content.decode()
@@ -515,7 +515,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
mock_create_missing_html_snapshots.return_value = 5
form_data = {}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
html = response.content.decode()
@@ -537,7 +537,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"guest_profile_user": selectable_user.id,
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
self.assertEqual(response.status_code, 200)
self.assertSuccessMessage(response.content.decode(), "Global settings updated")
@@ -553,7 +553,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"guest_profile_user": "",
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
self.assertEqual(response.status_code, 200)
self.assertSuccessMessage(response.content.decode(), "Global settings updated")
@@ -573,7 +573,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS,
}
response = self.client.post(
reverse("bookmarks:settings.update"), form_data, follow=True
reverse("linkding:settings.update"), form_data, follow=True
)
self.assertEqual(response.status_code, 200)
self.assertSuccessMessage(
@@ -585,15 +585,15 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
"update_global_settings": "",
"landing_page": GlobalSettings.LANDING_PAGE_SHARED_BOOKMARKS,
}
response = self.client.post(reverse("bookmarks:settings.update"), form_data)
response = self.client.post(reverse("linkding:settings.update"), form_data)
self.assertEqual(response.status_code, 403)
def test_global_settings_only_visible_for_superuser(self):
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
html = response.content.decode()
self.assertInHTML(
"<h2>Global settings</h2>",
'<h2 id="global-settings-heading">Global settings</h2>',
html,
count=0,
)
@@ -601,11 +601,11 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
superuser = self.setup_superuser()
self.client.force_login(superuser)
response = self.client.get(reverse("bookmarks:settings.general"))
response = self.client.get(reverse("linkding:settings.general"))
html = response.content.decode()
self.assertInHTML(
"<h2>Global settings</h2>",
'<h2 id="global-settings-heading">Global settings</h2>',
html,
count=1,
)

Some files were not shown because too many files have changed in this diff Show More