Add black code formatter

This commit is contained in:
Sascha Ißbrücker 2024-01-27 11:29:16 +01:00
parent 6775633be5
commit 98b9a9c1a0
128 changed files with 7181 additions and 4264 deletions

15
Makefile Normal file
View file

@ -0,0 +1,15 @@
.PHONY: serve
serve:
python manage.py runserver
tasks:
python manage.py process_tasks
test:
pytest
format:
black bookmarks
black siteroot
npx prettier bookmarks/frontend --write

View file

@ -281,6 +281,20 @@ python3 manage.py runserver
``` ```
The frontend is now available under http://localhost:8000 The frontend is now available under http://localhost:8000
### Tests
Run all tests with pytest:
```
pytest
```
### Formatting
Format Python code with black, and JavaScript code with prettier:
```
make format
```
### DevContainers ### DevContainers
This repository also supports DevContainers: [![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=git@github.com:sissbruecker/linkding.git) This repository also supports DevContainers: [![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=git@github.com:sissbruecker/linkding.git)
@ -300,8 +314,3 @@ Start the Django development server with:
python3 manage.py runserver python3 manage.py runserver
``` ```
The frontend is now available under http://localhost:8000 The frontend is now available under http://localhost:8000
Run all tests with pytest
```
pytest
```

View file

@ -14,80 +14,123 @@ from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
class LinkdingAdminSite(AdminSite): class LinkdingAdminSite(AdminSite):
site_header = 'linkding administration' site_header = "linkding administration"
site_title = 'linkding Admin' site_title = "linkding Admin"
class AdminBookmark(admin.ModelAdmin): class AdminBookmark(admin.ModelAdmin):
list_display = ('resolved_title', 'url', 'is_archived', 'owner', 'date_added') list_display = ("resolved_title", "url", "is_archived", "owner", "date_added")
search_fields = ('title', 'description', 'website_title', 'website_description', 'url', 'tags__name') search_fields = (
list_filter = ('owner__username', 'is_archived', 'unread', 'tags',) "title",
ordering = ('-date_added',) "description",
actions = ['delete_selected_bookmarks', 'archive_selected_bookmarks', 'unarchive_selected_bookmarks', 'mark_as_read', 'mark_as_unread'] "website_title",
"website_description",
"url",
"tags__name",
)
list_filter = (
"owner__username",
"is_archived",
"unread",
"tags",
)
ordering = ("-date_added",)
actions = [
"delete_selected_bookmarks",
"archive_selected_bookmarks",
"unarchive_selected_bookmarks",
"mark_as_read",
"mark_as_unread",
]
def get_actions(self, request): def get_actions(self, request):
actions = super().get_actions(request) actions = super().get_actions(request)
# Remove default delete action, which gets replaced by delete_selected_bookmarks below # Remove default delete action, which gets replaced by delete_selected_bookmarks below
# The default action shows a confirmation page which can fail in production when selecting all bookmarks and the # The default action shows a confirmation page which can fail in production when selecting all bookmarks and the
# number of objects to delete exceeds the value in DATA_UPLOAD_MAX_NUMBER_FIELDS (1000 by default) # number of objects to delete exceeds the value in DATA_UPLOAD_MAX_NUMBER_FIELDS (1000 by default)
del actions['delete_selected'] del actions["delete_selected"]
return actions return actions
def delete_selected_bookmarks(self, request, queryset: QuerySet): def delete_selected_bookmarks(self, request, queryset: QuerySet):
bookmarks_count = queryset.count() bookmarks_count = queryset.count()
for bookmark in queryset: for bookmark in queryset:
bookmark.delete() bookmark.delete()
self.message_user(request, ngettext( self.message_user(
'%d bookmark was successfully deleted.', request,
'%d bookmarks were successfully deleted.', ngettext(
"%d bookmark was successfully deleted.",
"%d bookmarks were successfully deleted.",
bookmarks_count, bookmarks_count,
) % bookmarks_count, messages.SUCCESS) )
% bookmarks_count,
messages.SUCCESS,
)
def archive_selected_bookmarks(self, request, queryset: QuerySet): def archive_selected_bookmarks(self, request, queryset: QuerySet):
for bookmark in queryset: for bookmark in queryset:
archive_bookmark(bookmark) archive_bookmark(bookmark)
bookmarks_count = queryset.count() bookmarks_count = queryset.count()
self.message_user(request, ngettext( self.message_user(
'%d bookmark was successfully archived.', request,
'%d bookmarks were successfully archived.', ngettext(
"%d bookmark was successfully archived.",
"%d bookmarks were successfully archived.",
bookmarks_count, bookmarks_count,
) % bookmarks_count, messages.SUCCESS) )
% bookmarks_count,
messages.SUCCESS,
)
def unarchive_selected_bookmarks(self, request, queryset: QuerySet): def unarchive_selected_bookmarks(self, request, queryset: QuerySet):
for bookmark in queryset: for bookmark in queryset:
unarchive_bookmark(bookmark) unarchive_bookmark(bookmark)
bookmarks_count = queryset.count() bookmarks_count = queryset.count()
self.message_user(request, ngettext( self.message_user(
'%d bookmark was successfully unarchived.', request,
'%d bookmarks were successfully unarchived.', ngettext(
"%d bookmark was successfully unarchived.",
"%d bookmarks were successfully unarchived.",
bookmarks_count, bookmarks_count,
) % bookmarks_count, messages.SUCCESS) )
% bookmarks_count,
messages.SUCCESS,
)
def mark_as_read(self, request, queryset: QuerySet): def mark_as_read(self, request, queryset: QuerySet):
bookmarks_count = queryset.count() bookmarks_count = queryset.count()
queryset.update(unread=False) queryset.update(unread=False)
self.message_user(request, ngettext( self.message_user(
'%d bookmark marked as read.', request,
'%d bookmarks marked as read.', ngettext(
"%d bookmark marked as read.",
"%d bookmarks marked as read.",
bookmarks_count, bookmarks_count,
) % bookmarks_count, messages.SUCCESS) )
% bookmarks_count,
messages.SUCCESS,
)
def mark_as_unread(self, request, queryset: QuerySet): def mark_as_unread(self, request, queryset: QuerySet):
bookmarks_count = queryset.count() bookmarks_count = queryset.count()
queryset.update(unread=True) queryset.update(unread=True)
self.message_user(request, ngettext( self.message_user(
'%d bookmark marked as unread.', request,
'%d bookmarks marked as unread.', ngettext(
"%d bookmark marked as unread.",
"%d bookmarks marked as unread.",
bookmarks_count, bookmarks_count,
) % bookmarks_count, messages.SUCCESS) )
% bookmarks_count,
messages.SUCCESS,
)
class AdminTag(admin.ModelAdmin): class AdminTag(admin.ModelAdmin):
list_display = ('name', 'bookmarks_count', 'owner', 'date_added') list_display = ("name", "bookmarks_count", "owner", "date_added")
search_fields = ('name', 'owner__username') search_fields = ("name", "owner__username")
list_filter = ('owner__username',) list_filter = ("owner__username",)
ordering = ('-date_added',) ordering = ("-date_added",)
actions = ['delete_unused_tags'] actions = ["delete_unused_tags"]
def get_queryset(self, request): def get_queryset(self, request):
queryset = super().get_queryset(request) queryset = super().get_queryset(request)
@ -97,7 +140,7 @@ class AdminTag(admin.ModelAdmin):
def bookmarks_count(self, obj): def bookmarks_count(self, obj):
return obj.bookmarks_count return obj.bookmarks_count
bookmarks_count.admin_order_field = 'bookmarks_count' bookmarks_count.admin_order_field = "bookmarks_count"
def delete_unused_tags(self, request, queryset: QuerySet): def delete_unused_tags(self, request, queryset: QuerySet):
unused_tags = queryset.filter(bookmark__isnull=True) unused_tags = queryset.filter(bookmark__isnull=True)
@ -106,23 +149,33 @@ class AdminTag(admin.ModelAdmin):
tag.delete() tag.delete()
if unused_tags_count > 0: if unused_tags_count > 0:
self.message_user(request, ngettext( self.message_user(
'%d unused tag was successfully deleted.', request,
'%d unused tags were successfully deleted.', ngettext(
"%d unused tag was successfully deleted.",
"%d unused tags were successfully deleted.",
unused_tags_count, unused_tags_count,
) % unused_tags_count, messages.SUCCESS) )
% unused_tags_count,
messages.SUCCESS,
)
else: else:
self.message_user(request, gettext( self.message_user(
'There were no unused tags in the selection', request,
), messages.SUCCESS) gettext(
"There were no unused tags in the selection",
),
messages.SUCCESS,
)
class AdminUserProfileInline(admin.StackedInline): class AdminUserProfileInline(admin.StackedInline):
model = UserProfile model = UserProfile
can_delete = False can_delete = False
verbose_name_plural = 'Profile' verbose_name_plural = "Profile"
fk_name = 'user' fk_name = "user"
readonly_fields = ('search_preferences', ) readonly_fields = ("search_preferences",)
class AdminCustomUser(UserAdmin): class AdminCustomUser(UserAdmin):
inlines = (AdminUserProfileInline,) inlines = (AdminUserProfileInline,)
@ -134,15 +187,15 @@ class AdminCustomUser(UserAdmin):
class AdminToast(admin.ModelAdmin): class AdminToast(admin.ModelAdmin):
list_display = ('key', 'message', 'owner', 'acknowledged') list_display = ("key", "message", "owner", "acknowledged")
search_fields = ('key', 'message') search_fields = ("key", "message")
list_filter = ('owner__username',) list_filter = ("owner__username",)
class AdminFeedToken(admin.ModelAdmin): class AdminFeedToken(admin.ModelAdmin):
list_display = ('key', 'user') list_display = ("key", "user")
search_fields = ['key'] search_fields = ["key"]
list_filter = ('user__username',) list_filter = ("user__username",)
linkding_admin_site = LinkdingAdminSite() linkding_admin_site = LinkdingAdminSite()

View file

@ -5,18 +5,28 @@ from rest_framework.response import Response
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from bookmarks import queries from bookmarks import queries
from bookmarks.api.serializers import BookmarkSerializer, TagSerializer, UserProfileSerializer from bookmarks.api.serializers import (
BookmarkSerializer,
TagSerializer,
UserProfileSerializer,
)
from bookmarks.models import Bookmark, BookmarkSearch, Tag, User from bookmarks.models import Bookmark, BookmarkSearch, Tag, User
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark, website_loader from bookmarks.services.bookmarks import (
archive_bookmark,
unarchive_bookmark,
website_loader,
)
from bookmarks.services.website_loader import WebsiteMetadata from bookmarks.services.website_loader import WebsiteMetadata
class BookmarkViewSet(viewsets.GenericViewSet, class BookmarkViewSet(
viewsets.GenericViewSet,
mixins.ListModelMixin, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.CreateModelMixin, mixins.CreateModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin): mixins.DestroyModelMixin,
):
serializer_class = BookmarkSerializer serializer_class = BookmarkSerializer
def get_permissions(self): def get_permissions(self):
@ -24,7 +34,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
# The shared action should still filter bookmarks so that # The shared action should still filter bookmarks so that
# unauthenticated users only see bookmarks from users that have public # unauthenticated users only see bookmarks from users that have public
# sharing explicitly enabled # sharing explicitly enabled
if self.action == 'shared': if self.action == "shared":
return [AllowAny()] return [AllowAny()]
# Otherwise use default permissions which should require authentication # Otherwise use default permissions which should require authentication
@ -33,7 +43,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
# For list action, use query set that applies search and tag projections # For list action, use query set that applies search and tag projections
if self.action == 'list': if self.action == "list":
search = BookmarkSearch.from_request(self.request.GET) search = BookmarkSearch.from_request(self.request.GET)
return queries.query_bookmarks(user, user.profile, search) return queries.query_bookmarks(user, user.profile, search)
@ -41,9 +51,9 @@ class BookmarkViewSet(viewsets.GenericViewSet,
return Bookmark.objects.all().filter(owner=user) return Bookmark.objects.all().filter(owner=user)
def get_serializer_context(self): def get_serializer_context(self):
return {'user': self.request.user} return {"user": self.request.user}
@action(methods=['get'], detail=False) @action(methods=["get"], detail=False)
def archived(self, request): def archived(self, request):
user = request.user user = request.user
search = BookmarkSearch.from_request(request.GET) search = BookmarkSearch.from_request(request.GET)
@ -53,51 +63,59 @@ class BookmarkViewSet(viewsets.GenericViewSet,
data = serializer(page, many=True).data data = serializer(page, many=True).data
return self.get_paginated_response(data) return self.get_paginated_response(data)
@action(methods=['get'], detail=False) @action(methods=["get"], detail=False)
def shared(self, request): def shared(self, request):
search = BookmarkSearch.from_request(request.GET) search = BookmarkSearch.from_request(request.GET)
user = User.objects.filter(username=search.user).first() user = User.objects.filter(username=search.user).first()
public_only = not request.user.is_authenticated public_only = not request.user.is_authenticated
query_set = queries.query_shared_bookmarks(user, request.user_profile, search, public_only) query_set = queries.query_shared_bookmarks(
user, request.user_profile, search, public_only
)
page = self.paginate_queryset(query_set) page = self.paginate_queryset(query_set)
serializer = self.get_serializer_class() serializer = self.get_serializer_class()
data = serializer(page, many=True).data data = serializer(page, many=True).data
return self.get_paginated_response(data) return self.get_paginated_response(data)
@action(methods=['post'], detail=True) @action(methods=["post"], detail=True)
def archive(self, request, pk): def archive(self, request, pk):
bookmark = self.get_object() bookmark = self.get_object()
archive_bookmark(bookmark) archive_bookmark(bookmark)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@action(methods=['post'], detail=True) @action(methods=["post"], detail=True)
def unarchive(self, request, pk): def unarchive(self, request, pk):
bookmark = self.get_object() bookmark = self.get_object()
unarchive_bookmark(bookmark) unarchive_bookmark(bookmark)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@action(methods=['get'], detail=False) @action(methods=["get"], detail=False)
def check(self, request): def check(self, request):
url = request.GET.get('url') url = request.GET.get("url")
bookmark = Bookmark.objects.filter(owner=request.user, url=url).first() bookmark = Bookmark.objects.filter(owner=request.user, url=url).first()
existing_bookmark_data = self.get_serializer(bookmark).data if bookmark else None existing_bookmark_data = (
self.get_serializer(bookmark).data if bookmark else None
)
# Either return metadata from existing bookmark, or scrape from URL # Either return metadata from existing bookmark, or scrape from URL
if bookmark: if bookmark:
metadata = WebsiteMetadata(url, bookmark.website_title, bookmark.website_description) metadata = WebsiteMetadata(
url, bookmark.website_title, bookmark.website_description
)
else: else:
metadata = website_loader.load_website_metadata(url) metadata = website_loader.load_website_metadata(url)
return Response({ return Response(
'bookmark': existing_bookmark_data, {"bookmark": existing_bookmark_data, "metadata": metadata.to_dict()},
'metadata': metadata.to_dict() status=status.HTTP_200_OK,
}, status=status.HTTP_200_OK) )
class TagViewSet(viewsets.GenericViewSet, class TagViewSet(
viewsets.GenericViewSet,
mixins.ListModelMixin, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.CreateModelMixin): mixins.CreateModelMixin,
):
serializer_class = TagSerializer serializer_class = TagSerializer
def get_queryset(self): def get_queryset(self):
@ -105,16 +123,16 @@ class TagViewSet(viewsets.GenericViewSet,
return Tag.objects.all().filter(owner=user) return Tag.objects.all().filter(owner=user)
def get_serializer_context(self): def get_serializer_context(self):
return {'user': self.request.user} return {"user": self.request.user}
class UserViewSet(viewsets.GenericViewSet): class UserViewSet(viewsets.GenericViewSet):
@action(methods=['get'], detail=False) @action(methods=["get"], detail=False)
def profile(self, request): def profile(self, request):
return Response(UserProfileSerializer(request.user.profile).data) return Response(UserProfileSerializer(request.user.profile).data)
router = DefaultRouter() router = DefaultRouter()
router.register(r'bookmarks', BookmarkViewSet, basename='bookmark') router.register(r"bookmarks", BookmarkViewSet, basename="bookmark")
router.register(r'tags', TagViewSet, basename='tag') router.register(r"tags", TagViewSet, basename="tag")
router.register(r'user', UserViewSet, basename='user') router.register(r"user", UserViewSet, basename="user")

View file

@ -14,7 +14,7 @@ class TagListField(serializers.ListField):
class BookmarkListSerializer(ListSerializer): class BookmarkListSerializer(ListSerializer):
def to_representation(self, data): def to_representation(self, data):
# Prefetch nested relations to avoid n+1 queries # Prefetch nested relations to avoid n+1 queries
prefetch_related_objects(data, 'tags') prefetch_related_objects(data, "tags")
return super().to_representation(data) return super().to_representation(data)
@ -23,32 +23,32 @@ class BookmarkSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Bookmark model = Bookmark
fields = [ fields = [
'id', "id",
'url', "url",
'title', "title",
'description', "description",
'notes', "notes",
'website_title', "website_title",
'website_description', "website_description",
'is_archived', "is_archived",
'unread', "unread",
'shared', "shared",
'tag_names', "tag_names",
'date_added', "date_added",
'date_modified' "date_modified",
] ]
read_only_fields = [ read_only_fields = [
'website_title', "website_title",
'website_description', "website_description",
'date_added', "date_added",
'date_modified' "date_modified",
] ]
list_serializer_class = BookmarkListSerializer list_serializer_class = BookmarkListSerializer
# Override optional char fields to provide default value # Override optional char fields to provide default value
title = serializers.CharField(required=False, allow_blank=True, default='') title = serializers.CharField(required=False, allow_blank=True, default="")
description = serializers.CharField(required=False, allow_blank=True, default='') description = serializers.CharField(required=False, allow_blank=True, default="")
notes = serializers.CharField(required=False, allow_blank=True, default='') notes = serializers.CharField(required=False, allow_blank=True, default="")
is_archived = serializers.BooleanField(required=False, default=False) is_archived = serializers.BooleanField(required=False, default=False)
unread = serializers.BooleanField(required=False, default=False) unread = serializers.BooleanField(required=False, default=False)
shared = serializers.BooleanField(required=False, default=False) shared = serializers.BooleanField(required=False, default=False)
@ -57,38 +57,38 @@ class BookmarkSerializer(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
bookmark = Bookmark() bookmark = Bookmark()
bookmark.url = validated_data['url'] bookmark.url = validated_data["url"]
bookmark.title = validated_data['title'] bookmark.title = validated_data["title"]
bookmark.description = validated_data['description'] bookmark.description = validated_data["description"]
bookmark.notes = validated_data['notes'] bookmark.notes = validated_data["notes"]
bookmark.is_archived = validated_data['is_archived'] bookmark.is_archived = validated_data["is_archived"]
bookmark.unread = validated_data['unread'] bookmark.unread = validated_data["unread"]
bookmark.shared = validated_data['shared'] bookmark.shared = validated_data["shared"]
tag_string = build_tag_string(validated_data['tag_names']) tag_string = build_tag_string(validated_data["tag_names"])
return create_bookmark(bookmark, tag_string, self.context['user']) return create_bookmark(bookmark, tag_string, self.context["user"])
def update(self, instance: Bookmark, validated_data): def update(self, instance: Bookmark, validated_data):
# Update fields if they were provided in the payload # Update fields if they were provided in the payload
for key in ['url', 'title', 'description', 'notes', 'unread', 'shared']: for key in ["url", "title", "description", "notes", "unread", "shared"]:
if key in validated_data: if key in validated_data:
setattr(instance, key, validated_data[key]) setattr(instance, key, validated_data[key])
# Use tag string from payload, or use bookmark's current tags as fallback # Use tag string from payload, or use bookmark's current tags as fallback
tag_string = build_tag_string(instance.tag_names) tag_string = build_tag_string(instance.tag_names)
if 'tag_names' in validated_data: if "tag_names" in validated_data:
tag_string = build_tag_string(validated_data['tag_names']) tag_string = build_tag_string(validated_data["tag_names"])
return update_bookmark(instance, tag_string, self.context['user']) return update_bookmark(instance, tag_string, self.context["user"])
class TagSerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Tag model = Tag
fields = ['id', 'name', 'date_added'] fields = ["id", "name", "date_added"]
read_only_fields = ['date_added'] read_only_fields = ["date_added"]
def create(self, validated_data): def create(self, validated_data):
return get_or_create_tag(validated_data['name'], self.context['user']) return get_or_create_tag(validated_data["name"], self.context["user"])
class UserProfileSerializer(serializers.ModelSerializer): class UserProfileSerializer(serializers.ModelSerializer):

View file

@ -2,7 +2,7 @@ from django.apps import AppConfig
class BookmarksConfig(AppConfig): class BookmarksConfig(AppConfig):
name = 'bookmarks' name = "bookmarks"
def ready(self): def ready(self):
# Register signal handlers # Register signal handlers

View file

@ -5,28 +5,32 @@ from bookmarks import utils
def toasts(request): def toasts(request):
user = request.user user = request.user
toast_messages = Toast.objects.filter(owner=user, acknowledged=False) if user.is_authenticated else [] toast_messages = (
Toast.objects.filter(owner=user, acknowledged=False)
if user.is_authenticated
else []
)
has_toasts = len(toast_messages) > 0 has_toasts = len(toast_messages) > 0
return { return {
'has_toasts': has_toasts, "has_toasts": has_toasts,
'toast_messages': toast_messages, "toast_messages": toast_messages,
} }
def public_shares(request): def public_shares(request):
# Only check for public shares for anonymous users # Only check for public shares for anonymous users
if not request.user.is_authenticated: if not request.user.is_authenticated:
query_set = queries.query_shared_bookmarks(None, request.user_profile, BookmarkSearch(), True) query_set = queries.query_shared_bookmarks(
None, request.user_profile, BookmarkSearch(), True
)
has_public_shares = query_set.count() > 0 has_public_shares = query_set.count() > 0
return { return {
'has_public_shares': has_public_shares, "has_public_shares": has_public_shares,
} }
return {} return {}
def app_version(request): def app_version(request):
return { return {"app_version": utils.app_version}
'app_version': utils.app_version
}

View file

@ -6,38 +6,54 @@ from bookmarks.e2e.helpers import LinkdingE2ETestCase
class BookmarkFormE2ETestCase(LinkdingE2ETestCase): class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
def test_create_should_check_for_existing_bookmark(self): def test_create_should_check_for_existing_bookmark(self):
existing_bookmark = self.setup_bookmark(title='Existing title', existing_bookmark = self.setup_bookmark(
description='Existing description', title="Existing title",
notes='Existing notes', description="Existing description",
tags=[self.setup_tag(name='tag1'), self.setup_tag(name='tag2')], notes="Existing notes",
website_title='Existing website title', tags=[self.setup_tag(name="tag1"), self.setup_tag(name="tag2")],
website_description='Existing website description', website_title="Existing website title",
unread=True) website_description="Existing website description",
tag_names = ' '.join(existing_bookmark.tag_names) unread=True,
)
tag_names = " ".join(existing_bookmark.tag_names)
with sync_playwright() as p: with sync_playwright() as p:
browser = self.setup_browser(p) browser = self.setup_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto(self.live_server_url + reverse('bookmarks:new')) page.goto(self.live_server_url + reverse("bookmarks:new"))
# Enter bookmarked URL # Enter bookmarked URL
page.get_by_label('URL').fill(existing_bookmark.url) page.get_by_label("URL").fill(existing_bookmark.url)
# Already bookmarked hint should be visible # Already bookmarked hint should be visible
page.get_by_text('This URL is already bookmarked.').wait_for(timeout=2000) page.get_by_text("This URL is already bookmarked.").wait_for(timeout=2000)
# Form should be pre-filled with data from existing bookmark # Form should be pre-filled with data from existing bookmark
self.assertEqual(existing_bookmark.title, page.get_by_label('Title').input_value()) self.assertEqual(
self.assertEqual(existing_bookmark.description, page.get_by_label('Description').input_value()) existing_bookmark.title, page.get_by_label("Title").input_value()
self.assertEqual(existing_bookmark.notes, page.get_by_label('Notes').input_value()) )
self.assertEqual(existing_bookmark.website_title, page.get_by_label('Title').get_attribute('placeholder')) self.assertEqual(
self.assertEqual(existing_bookmark.website_description, existing_bookmark.description,
page.get_by_label('Description').get_attribute('placeholder')) page.get_by_label("Description").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()) self.assertEqual(
existing_bookmark.notes, page.get_by_label("Notes").input_value()
)
self.assertEqual(
existing_bookmark.website_title,
page.get_by_label("Title").get_attribute("placeholder"),
)
self.assertEqual(
existing_bookmark.website_description,
page.get_by_label("Description").get_attribute("placeholder"),
)
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 # Enter non-bookmarked URL
page.get_by_label('URL').fill('https://example.com/unknown') page.get_by_label("URL").fill("https://example.com/unknown")
# Already bookmarked hint should be hidden # Already bookmarked hint should be hidden
page.get_by_text('This URL is already bookmarked.').wait_for(state='hidden', timeout=2000) page.get_by_text("This URL is already bookmarked.").wait_for(
state="hidden", timeout=2000
)
browser.close() browser.close()
@ -47,21 +63,25 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
with sync_playwright() as p: with sync_playwright() as p:
browser = self.setup_browser(p) browser = self.setup_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto(self.live_server_url + reverse('bookmarks:edit', args=[bookmark.id])) page.goto(
self.live_server_url + reverse("bookmarks:edit", args=[bookmark.id])
)
page.wait_for_timeout(timeout=1000) page.wait_for_timeout(timeout=1000)
page.get_by_text('This URL is already bookmarked.').wait_for(state='hidden') page.get_by_text("This URL is already bookmarked.").wait_for(state="hidden")
def test_enter_url_of_existing_bookmark_should_show_notes(self): def test_enter_url_of_existing_bookmark_should_show_notes(self):
bookmark = self.setup_bookmark(notes='Existing notes', description='Existing description') bookmark = self.setup_bookmark(
notes="Existing notes", description="Existing description"
)
with sync_playwright() as p: with sync_playwright() as p:
browser = self.setup_browser(p) browser = self.setup_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto(self.live_server_url + reverse('bookmarks:new')) page.goto(self.live_server_url + reverse("bookmarks:new"))
details = page.locator('details.notes') details = page.locator("details.notes")
expect(details).not_to_have_attribute('open', value='') expect(details).not_to_have_attribute("open", value="")
page.get_by_label('URL').fill(bookmark.url) page.get_by_label("URL").fill(bookmark.url)
expect(details).to_have_attribute('open', value='') expect(details).to_have_attribute("open", value="")

View file

@ -9,15 +9,15 @@ from bookmarks.e2e.helpers import LinkdingE2ETestCase
class BookmarkItemE2ETestCase(LinkdingE2ETestCase): class BookmarkItemE2ETestCase(LinkdingE2ETestCase):
@skip("Fails in CI, needs investigation") @skip("Fails in CI, needs investigation")
def test_toggle_notes_should_show_hide_notes(self): def test_toggle_notes_should_show_hide_notes(self):
bookmark = self.setup_bookmark(notes='Test notes') bookmark = self.setup_bookmark(notes="Test notes")
with sync_playwright() as p: with sync_playwright() as p:
page = self.open(reverse('bookmarks:index'), p) page = self.open(reverse("bookmarks:index"), p)
notes = self.locate_bookmark(bookmark.title).locator('.notes') notes = self.locate_bookmark(bookmark.title).locator(".notes")
expect(notes).to_be_hidden() expect(notes).to_be_hidden()
toggle_notes = page.locator('li button.toggle-notes') toggle_notes = page.locator("li button.toggle-notes")
toggle_notes.click() toggle_notes.click()
expect(notes).to_be_visible() expect(notes).to_be_visible()

View file

@ -9,100 +9,180 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
def setup_test_data(self): def setup_test_data(self):
self.setup_numbered_bookmarks(50) self.setup_numbered_bookmarks(50)
self.setup_numbered_bookmarks(50, archived=True) self.setup_numbered_bookmarks(50, archived=True)
self.setup_numbered_bookmarks(50, prefix='foo') self.setup_numbered_bookmarks(50, prefix="foo")
self.setup_numbered_bookmarks(50, archived=True, prefix='foo') self.setup_numbered_bookmarks(50, archived=True, prefix="foo")
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count()) self.assertEqual(
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count()) 50,
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count()) Bookmark.objects.filter(
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count()) is_archived=False, title__startswith="Bookmark"
).count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(
is_archived=True, title__startswith="Archived Bookmark"
).count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
)
def test_active_bookmarks_bulk_select_across(self): def test_active_bookmarks_bulk_select_across(self):
self.setup_test_data() self.setup_test_data()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:index'), p) self.open(reverse("bookmarks:index"), p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click() self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click() self.locate_bulk_edit_select_across().click()
self.select_bulk_action('Delete') self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text('Execute').click() self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.assertEqual(0, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count()) self.assertEqual(
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count()) 0,
self.assertEqual(0, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count()) Bookmark.objects.filter(
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count()) is_archived=False, title__startswith="Bookmark"
).count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(
is_archived=True, title__startswith="Archived Bookmark"
).count(),
)
self.assertEqual(
0,
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
)
def test_archived_bookmarks_bulk_select_across(self): def test_archived_bookmarks_bulk_select_across(self):
self.setup_test_data() self.setup_test_data()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:archived'), p) self.open(reverse("bookmarks:archived"), p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click() self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click() self.locate_bulk_edit_select_across().click()
self.select_bulk_action('Delete') self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text('Execute').click() self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count()) self.assertEqual(
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count()) 50,
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count()) Bookmark.objects.filter(
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count()) is_archived=False, title__startswith="Bookmark"
).count(),
)
self.assertEqual(
0,
Bookmark.objects.filter(
is_archived=True, title__startswith="Archived Bookmark"
).count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
)
self.assertEqual(
0,
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
)
def test_active_bookmarks_bulk_select_across_respects_query(self): def test_active_bookmarks_bulk_select_across_respects_query(self):
self.setup_test_data() self.setup_test_data()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:index') + '?q=foo', p) self.open(reverse("bookmarks:index") + "?q=foo", p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click() self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click() self.locate_bulk_edit_select_across().click()
self.select_bulk_action('Delete') self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text('Execute').click() self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count()) self.assertEqual(
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count()) 50,
self.assertEqual(0, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count()) Bookmark.objects.filter(
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count()) is_archived=False, title__startswith="Bookmark"
).count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(
is_archived=True, title__startswith="Archived Bookmark"
).count(),
)
self.assertEqual(
0,
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
)
def test_archived_bookmarks_bulk_select_across_respects_query(self): def test_archived_bookmarks_bulk_select_across_respects_query(self):
self.setup_test_data() self.setup_test_data()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:archived') + '?q=foo', p) self.open(reverse("bookmarks:archived") + "?q=foo", p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click() self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click() self.locate_bulk_edit_select_across().click()
self.select_bulk_action('Delete') self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text('Execute').click() self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='Bookmark').count()) self.assertEqual(
self.assertEqual(50, Bookmark.objects.filter(is_archived=True, title__startswith='Archived Bookmark').count()) 50,
self.assertEqual(50, Bookmark.objects.filter(is_archived=False, title__startswith='foo').count()) Bookmark.objects.filter(
self.assertEqual(0, Bookmark.objects.filter(is_archived=True, title__startswith='foo').count()) is_archived=False, title__startswith="Bookmark"
).count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(
is_archived=True, title__startswith="Archived Bookmark"
).count(),
)
self.assertEqual(
50,
Bookmark.objects.filter(is_archived=False, title__startswith="foo").count(),
)
self.assertEqual(
0,
Bookmark.objects.filter(is_archived=True, title__startswith="foo").count(),
)
def test_select_all_toggles_all_checkboxes(self): def test_select_all_toggles_all_checkboxes(self):
self.setup_numbered_bookmarks(5) self.setup_numbered_bookmarks(5)
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
page = self.open(url, p) page = self.open(url, p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
checkboxes = page.locator('label[ld-bulk-edit-checkbox] input') checkboxes = page.locator("label[ld-bulk-edit-checkbox] input")
self.assertEqual(6, checkboxes.count()) self.assertEqual(6, checkboxes.count())
for i in range(checkboxes.count()): for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).not_to_be_checked() expect(checkboxes.nth(i)).not_to_be_checked()
@ -121,7 +201,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
self.setup_numbered_bookmarks(5) self.setup_numbered_bookmarks(5)
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
self.open(url, p) self.open(url, p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
@ -138,7 +218,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
self.setup_numbered_bookmarks(5) self.setup_numbered_bookmarks(5)
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
self.open(url, p) self.open(url, p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
@ -160,7 +240,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
self.setup_numbered_bookmarks(5) self.setup_numbered_bookmarks(5)
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
self.open(url, p) self.open(url, p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
@ -171,18 +251,22 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
expect(self.locate_bulk_edit_select_across()).to_be_checked() expect(self.locate_bulk_edit_select_across()).to_be_checked()
# Hide select across by toggling a single bookmark # Hide select across by toggling a single bookmark
self.locate_bookmark('Bookmark 1').locator('label[ld-bulk-edit-checkbox]').click() self.locate_bookmark("Bookmark 1").locator(
"label[ld-bulk-edit-checkbox]"
).click()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible() expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
# Show select across again, verify it is unchecked # Show select across again, verify it is unchecked
self.locate_bookmark('Bookmark 1').locator('label[ld-bulk-edit-checkbox]').click() self.locate_bookmark("Bookmark 1").locator(
"label[ld-bulk-edit-checkbox]"
).click()
expect(self.locate_bulk_edit_select_across()).not_to_be_checked() expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
def test_execute_resets_all_checkboxes(self): def test_execute_resets_all_checkboxes(self):
self.setup_numbered_bookmarks(100) self.setup_numbered_bookmarks(100)
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
page = self.open(url, p) page = self.open(url, p)
# Select all bookmarks, enable select across # Select all bookmarks, enable select across
@ -191,18 +275,18 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
self.locate_bulk_edit_select_across().click() self.locate_bulk_edit_select_across().click()
# Get reference for bookmark list # Get reference for bookmark list
bookmark_list = page.locator('ul[ld-bookmark-list]') bookmark_list = page.locator("ul[ld-bookmark-list]")
# Execute bulk action # Execute bulk action
self.select_bulk_action('Mark as unread') self.select_bulk_action("Mark as unread")
self.locate_bulk_edit_bar().get_by_text('Execute').click() self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.locate_bulk_edit_bar().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible) # Wait until bookmark list is updated (old reference becomes invisible)
expect(bookmark_list).not_to_be_visible() expect(bookmark_list).not_to_be_visible()
# Verify bulk edit checkboxes are reset # Verify bulk edit checkboxes are reset
checkboxes = page.locator('label[ld-bulk-edit-checkbox] input') checkboxes = page.locator("label[ld-bulk-edit-checkbox] input")
self.assertEqual(31, checkboxes.count()) self.assertEqual(31, checkboxes.count())
for i in range(checkboxes.count()): for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).not_to_be_checked() expect(checkboxes.nth(i)).not_to_be_checked()
@ -215,18 +299,22 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
self.setup_numbered_bookmarks(100) self.setup_numbered_bookmarks(100)
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
self.open(url, p) self.open(url, p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click() self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_bar().get_by_text('All pages (100 bookmarks)')).to_be_visible() expect(
self.locate_bulk_edit_bar().get_by_text("All pages (100 bookmarks)")
).to_be_visible()
self.select_bulk_action('Delete') self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text('Execute').click() self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.locate_bulk_edit_select_all().click() self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_bar().get_by_text('All pages (70 bookmarks)')).to_be_visible() expect(
self.locate_bulk_edit_bar().get_by_text("All pages (70 bookmarks)")
).to_be_visible()

View file

@ -16,13 +16,15 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
# verify correct data is loaded on update # verify correct data is loaded on update
self.setup_numbered_bookmarks(3, with_tags=True) self.setup_numbered_bookmarks(3, with_tags=True)
self.setup_numbered_bookmarks(3, with_tags=True, archived=True) self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
self.setup_numbered_bookmarks(3, self.setup_numbered_bookmarks(
3,
shared=True, shared=True,
prefix="Joe's Bookmark", prefix="Joe's Bookmark",
user=self.setup_user(enable_sharing=True)) user=self.setup_user(enable_sharing=True),
)
def assertVisibleBookmarks(self, titles: List[str]): def assertVisibleBookmarks(self, titles: List[str]):
bookmark_tags = self.page.locator('li[ld-bookmark-item]') bookmark_tags = self.page.locator("li[ld-bookmark-item]")
expect(bookmark_tags).to_have_count(len(titles)) expect(bookmark_tags).to_have_count(len(titles))
for title in titles: for title in titles:
@ -30,7 +32,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
expect(matching_tag).to_be_visible() expect(matching_tag).to_be_visible()
def assertVisibleTags(self, titles: List[str]): def assertVisibleTags(self, titles: List[str]):
tag_tags = self.page.locator('.tag-cloud .unselected-tags a') tag_tags = self.page.locator(".tag-cloud .unselected-tags a")
expect(tag_tags).to_have_count(len(titles)) expect(tag_tags).to_have_count(len(titles))
for title in titles: for title in titles:
@ -38,65 +40,67 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
expect(matching_tag).to_be_visible() expect(matching_tag).to_be_visible()
def test_partial_update_respects_query(self): def test_partial_update_respects_query(self):
self.setup_numbered_bookmarks(5, prefix='foo') self.setup_numbered_bookmarks(5, prefix="foo")
self.setup_numbered_bookmarks(5, prefix='bar') self.setup_numbered_bookmarks(5, prefix="bar")
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') + '?q=foo' url = reverse("bookmarks:index") + "?q=foo"
self.open(url, p) self.open(url, p)
self.assertVisibleBookmarks(['foo 1', 'foo 2', 'foo 3', 'foo 4', 'foo 5']) self.assertVisibleBookmarks(["foo 1", "foo 2", "foo 3", "foo 4", "foo 5"])
self.locate_bookmark('foo 2').get_by_text('Archive').click() self.locate_bookmark("foo 2").get_by_text("Archive").click()
self.assertVisibleBookmarks(['foo 1', 'foo 3', 'foo 4', 'foo 5']) self.assertVisibleBookmarks(["foo 1", "foo 3", "foo 4", "foo 5"])
def test_partial_update_respects_sort(self): def test_partial_update_respects_sort(self):
self.setup_numbered_bookmarks(5, prefix='foo') self.setup_numbered_bookmarks(5, prefix="foo")
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') + '?sort=title_asc' url = reverse("bookmarks:index") + "?sort=title_asc"
page = self.open(url, p) page = self.open(url, p)
first_item = page.locator('li[ld-bookmark-item]').first first_item = page.locator("li[ld-bookmark-item]").first
expect(first_item).to_contain_text('foo 1') expect(first_item).to_contain_text("foo 1")
first_item.get_by_text('Archive').click() first_item.get_by_text("Archive").click()
first_item = page.locator('li[ld-bookmark-item]').first first_item = page.locator("li[ld-bookmark-item]").first
expect(first_item).to_contain_text('foo 2') expect(first_item).to_contain_text("foo 2")
def test_partial_update_respects_page(self): def test_partial_update_respects_page(self):
# add a suffix, otherwise 'foo 1' also matches 'foo 10' # add a suffix, otherwise 'foo 1' also matches 'foo 10'
self.setup_numbered_bookmarks(50, prefix='foo', suffix='-') self.setup_numbered_bookmarks(50, prefix="foo", suffix="-")
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') + '?q=foo&page=2' url = reverse("bookmarks:index") + "?q=foo&page=2"
self.open(url, p) self.open(url, p)
# with descending sort, page two has 'foo 1' to 'foo 20' # with descending sort, page two has 'foo 1' to 'foo 20'
expected_titles = [f'foo {i}-' for i in range(1, 21)] expected_titles = [f"foo {i}-" for i in range(1, 21)]
self.assertVisibleBookmarks(expected_titles) self.assertVisibleBookmarks(expected_titles)
self.locate_bookmark('foo 20-').get_by_text('Archive').click() self.locate_bookmark("foo 20-").get_by_text("Archive").click()
expected_titles = [f'foo {i}-' for i in range(1, 20)] expected_titles = [f"foo {i}-" for i in range(1, 20)]
self.assertVisibleBookmarks(expected_titles) self.assertVisibleBookmarks(expected_titles)
def test_multiple_partial_updates(self): def test_multiple_partial_updates(self):
self.setup_numbered_bookmarks(5) self.setup_numbered_bookmarks(5)
with sync_playwright() as p: with sync_playwright() as p:
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
self.open(url, p) self.open(url, p)
self.locate_bookmark('Bookmark 1').get_by_text('Archive').click() self.locate_bookmark("Bookmark 1").get_by_text("Archive").click()
self.assertVisibleBookmarks(['Bookmark 2', 'Bookmark 3', 'Bookmark 4', 'Bookmark 5']) self.assertVisibleBookmarks(
["Bookmark 2", "Bookmark 3", "Bookmark 4", "Bookmark 5"]
)
self.locate_bookmark('Bookmark 2').get_by_text('Archive').click() self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
self.assertVisibleBookmarks(['Bookmark 3', 'Bookmark 4', 'Bookmark 5']) self.assertVisibleBookmarks(["Bookmark 3", "Bookmark 4", "Bookmark 5"])
self.locate_bookmark('Bookmark 3').get_by_text('Archive').click() self.locate_bookmark("Bookmark 3").get_by_text("Archive").click()
self.assertVisibleBookmarks(['Bookmark 4', 'Bookmark 5']) self.assertVisibleBookmarks(["Bookmark 4", "Bookmark 5"])
self.assertReloads(0) self.assertReloads(0)
@ -104,185 +108,201 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
self.setup_fixture() self.setup_fixture()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:index'), p) self.open(reverse("bookmarks:index"), p)
self.locate_bookmark('Bookmark 2').get_by_text('Archive').click() self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3']) self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(['Tag 1', 'Tag 3']) self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_active_bookmarks_partial_update_on_delete(self): def test_active_bookmarks_partial_update_on_delete(self):
self.setup_fixture() self.setup_fixture()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:index'), p) self.open(reverse("bookmarks:index"), p)
self.locate_bookmark('Bookmark 2').get_by_text('Remove').click() self.locate_bookmark("Bookmark 2").get_by_text("Remove").click()
self.locate_bookmark('Bookmark 2').get_by_text('Confirm').click() self.locate_bookmark("Bookmark 2").get_by_text("Confirm").click()
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3']) self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(['Tag 1', 'Tag 3']) self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_active_bookmarks_partial_update_on_mark_as_read(self): def test_active_bookmarks_partial_update_on_mark_as_read(self):
self.setup_fixture() self.setup_fixture()
bookmark2 = self.get_numbered_bookmark('Bookmark 2') bookmark2 = self.get_numbered_bookmark("Bookmark 2")
bookmark2.unread = True bookmark2.unread = True
bookmark2.save() bookmark2.save()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:index'), p) self.open(reverse("bookmarks:index"), p)
expect(self.locate_bookmark('Bookmark 2')).to_have_class('unread') expect(self.locate_bookmark("Bookmark 2")).to_have_class("unread")
self.locate_bookmark('Bookmark 2').get_by_text('Unread').click() self.locate_bookmark("Bookmark 2").get_by_text("Unread").click()
self.locate_bookmark('Bookmark 2').get_by_text('Yes').click() self.locate_bookmark("Bookmark 2").get_by_text("Yes").click()
expect(self.locate_bookmark('Bookmark 2')).not_to_have_class('unread') expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("unread")
self.assertReloads(0) self.assertReloads(0)
def test_active_bookmarks_partial_update_on_unshare(self): def test_active_bookmarks_partial_update_on_unshare(self):
self.setup_fixture() self.setup_fixture()
bookmark2 = self.get_numbered_bookmark('Bookmark 2') bookmark2 = self.get_numbered_bookmark("Bookmark 2")
bookmark2.shared = True bookmark2.shared = True
bookmark2.save() bookmark2.save()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:index'), p) self.open(reverse("bookmarks:index"), p)
expect(self.locate_bookmark('Bookmark 2')).to_have_class('shared') expect(self.locate_bookmark("Bookmark 2")).to_have_class("shared")
self.locate_bookmark('Bookmark 2').get_by_text('Shared').click() self.locate_bookmark("Bookmark 2").get_by_text("Shared").click()
self.locate_bookmark('Bookmark 2').get_by_text('Yes').click() self.locate_bookmark("Bookmark 2").get_by_text("Yes").click()
expect(self.locate_bookmark('Bookmark 2')).not_to_have_class('shared') expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("shared")
self.assertReloads(0) self.assertReloads(0)
def test_active_bookmarks_partial_update_on_bulk_archive(self): def test_active_bookmarks_partial_update_on_bulk_archive(self):
self.setup_fixture() self.setup_fixture()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:index'), p) self.open(reverse("bookmarks:index"), p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bookmark('Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click() self.locate_bookmark("Bookmark 2").locator(
self.select_bulk_action('Archive') "label[ld-bulk-edit-checkbox]"
self.locate_bulk_edit_bar().get_by_text('Execute').click() ).click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.select_bulk_action("Archive")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3']) self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(['Tag 1', 'Tag 3']) self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_active_bookmarks_partial_update_on_bulk_delete(self): def test_active_bookmarks_partial_update_on_bulk_delete(self):
self.setup_fixture() self.setup_fixture()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:index'), p) self.open(reverse("bookmarks:index"), p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bookmark('Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click() self.locate_bookmark("Bookmark 2").locator(
self.select_bulk_action('Delete') "label[ld-bulk-edit-checkbox]"
self.locate_bulk_edit_bar().get_by_text('Execute').click() ).click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3']) self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(['Tag 1', 'Tag 3']) self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_archived_bookmarks_partial_update_on_unarchive(self): def test_archived_bookmarks_partial_update_on_unarchive(self):
self.setup_fixture() self.setup_fixture()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:archived'), p) self.open(reverse("bookmarks:archived"), p)
self.locate_bookmark('Archived Bookmark 2').get_by_text('Unarchive').click() self.locate_bookmark("Archived Bookmark 2").get_by_text("Unarchive").click()
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3']) self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3']) self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_archived_bookmarks_partial_update_on_delete(self): def test_archived_bookmarks_partial_update_on_delete(self):
self.setup_fixture() self.setup_fixture()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:archived'), p) self.open(reverse("bookmarks:archived"), p)
self.locate_bookmark('Archived Bookmark 2').get_by_text('Remove').click() self.locate_bookmark("Archived Bookmark 2").get_by_text("Remove").click()
self.locate_bookmark('Archived Bookmark 2').get_by_text('Confirm').click() self.locate_bookmark("Archived Bookmark 2").get_by_text("Confirm").click()
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3']) self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3']) self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_archived_bookmarks_partial_update_on_bulk_unarchive(self): def test_archived_bookmarks_partial_update_on_bulk_unarchive(self):
self.setup_fixture() self.setup_fixture()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:archived'), p) self.open(reverse("bookmarks:archived"), p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bookmark('Archived Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click() self.locate_bookmark("Archived Bookmark 2").locator(
self.select_bulk_action('Unarchive') "label[ld-bulk-edit-checkbox]"
self.locate_bulk_edit_bar().get_by_text('Execute').click() ).click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.select_bulk_action("Unarchive")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3']) self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3']) self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_archived_bookmarks_partial_update_on_bulk_delete(self): def test_archived_bookmarks_partial_update_on_bulk_delete(self):
self.setup_fixture() self.setup_fixture()
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:archived'), p) self.open(reverse("bookmarks:archived"), p)
self.locate_bulk_edit_toggle().click() self.locate_bulk_edit_toggle().click()
self.locate_bookmark('Archived Bookmark 2').locator('label[ld-bulk-edit-checkbox]').click() self.locate_bookmark("Archived Bookmark 2").locator(
self.select_bulk_action('Delete') "label[ld-bulk-edit-checkbox]"
self.locate_bulk_edit_bar().get_by_text('Execute').click() ).click()
self.locate_bulk_edit_bar().get_by_text('Confirm').click() self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_bulk_edit_bar().get_by_text("Confirm").click()
self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3']) self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3']) self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_shared_bookmarks_partial_update_on_unarchive(self): def test_shared_bookmarks_partial_update_on_unarchive(self):
self.setup_fixture() self.setup_fixture()
self.setup_numbered_bookmarks(3, shared=True, prefix="My Bookmark", with_tags=True) self.setup_numbered_bookmarks(
3, shared=True, prefix="My Bookmark", with_tags=True
)
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:shared'), p) self.open(reverse("bookmarks:shared"), p)
self.locate_bookmark('My Bookmark 2').get_by_text('Archive').click() self.locate_bookmark("My Bookmark 2").get_by_text("Archive").click()
# Shared bookmarks page also shows archived bookmarks, though it probably shouldn't # Shared bookmarks page also shows archived bookmarks, though it probably shouldn't
self.assertVisibleBookmarks([ self.assertVisibleBookmarks(
'My Bookmark 1', [
'My Bookmark 2', "My Bookmark 1",
'My Bookmark 3', "My Bookmark 2",
"My Bookmark 3",
"Joe's Bookmark 1", "Joe's Bookmark 1",
"Joe's Bookmark 2", "Joe's Bookmark 2",
"Joe's Bookmark 3", "Joe's Bookmark 3",
]) ]
self.assertVisibleTags(['Shared Tag 1', 'Shared Tag 2', 'Shared Tag 3']) )
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 2", "Shared Tag 3"])
self.assertReloads(0) self.assertReloads(0)
def test_shared_bookmarks_partial_update_on_delete(self): def test_shared_bookmarks_partial_update_on_delete(self):
self.setup_fixture() self.setup_fixture()
self.setup_numbered_bookmarks(3, shared=True, prefix="My Bookmark", with_tags=True) self.setup_numbered_bookmarks(
3, shared=True, prefix="My Bookmark", with_tags=True
)
with sync_playwright() as p: with sync_playwright() as p:
self.open(reverse('bookmarks:shared'), p) self.open(reverse("bookmarks:shared"), p)
self.locate_bookmark('My Bookmark 2').get_by_text('Remove').click() self.locate_bookmark("My Bookmark 2").get_by_text("Remove").click()
self.locate_bookmark('My Bookmark 2').get_by_text('Confirm').click() self.locate_bookmark("My Bookmark 2").get_by_text("Confirm").click()
self.assertVisibleBookmarks([ self.assertVisibleBookmarks(
'My Bookmark 1', [
'My Bookmark 3', "My Bookmark 1",
"My Bookmark 3",
"Joe's Bookmark 1", "Joe's Bookmark 1",
"Joe's Bookmark 2", "Joe's Bookmark 2",
"Joe's Bookmark 3", "Joe's Bookmark 3",
]) ]
self.assertVisibleTags(['Shared Tag 1', 'Shared Tag 3']) )
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 3"])
self.assertReloads(0) self.assertReloads(0)

View file

@ -9,11 +9,11 @@ class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase):
with sync_playwright() as p: with sync_playwright() as p:
browser = self.setup_browser(p) browser = self.setup_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto(self.live_server_url + reverse('bookmarks:index')) page.goto(self.live_server_url + reverse("bookmarks:index"))
page.press('body', 's') page.press("body", "s")
expect(page.get_by_placeholder('Search for words or #tags')).to_be_focused() expect(page.get_by_placeholder("Search for words or #tags")).to_be_focused()
browser.close() browser.close()
@ -21,10 +21,10 @@ class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase):
with sync_playwright() as p: with sync_playwright() as p:
browser = self.setup_browser(p) browser = self.setup_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto(self.live_server_url + reverse('bookmarks:index')) page.goto(self.live_server_url + reverse("bookmarks:index"))
page.press('body', 'n') page.press("body", "n")
expect(page).to_have_url(self.live_server_url + reverse('bookmarks:new')) expect(page).to_have_url(self.live_server_url + reverse("bookmarks:new"))
browser.close() browser.close()

View file

@ -9,12 +9,14 @@ class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
with sync_playwright() as p: with sync_playwright() as p:
browser = self.setup_browser(p) browser = self.setup_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto(self.live_server_url + reverse('bookmarks:settings.general')) page.goto(self.live_server_url + reverse("bookmarks:settings.general"))
enable_sharing = page.get_by_label('Enable bookmark sharing') enable_sharing = page.get_by_label("Enable bookmark sharing")
enable_sharing_label = page.get_by_text('Enable bookmark sharing') enable_sharing_label = page.get_by_text("Enable bookmark sharing")
enable_public_sharing = page.get_by_label('Enable public bookmark sharing') enable_public_sharing = page.get_by_label("Enable public bookmark sharing")
enable_public_sharing_label = page.get_by_text('Enable public bookmark sharing') enable_public_sharing_label = page.get_by_text(
"Enable public bookmark sharing"
)
# Public sharing is disabled by default # Public sharing is disabled by default
expect(enable_sharing).not_to_be_checked() expect(enable_sharing).not_to_be_checked()

View file

@ -7,24 +7,28 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin): class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
def setUp(self) -> None: def setUp(self) -> None:
self.client.force_login(self.get_or_create_test_user()) self.client.force_login(self.get_or_create_test_user())
self.cookie = self.client.cookies['sessionid'] self.cookie = self.client.cookies["sessionid"]
def setup_browser(self, playwright) -> BrowserContext: def setup_browser(self, playwright) -> BrowserContext:
browser = playwright.chromium.launch(headless=True) browser = playwright.chromium.launch(headless=True)
context = browser.new_context() context = browser.new_context()
context.add_cookies([{ context.add_cookies(
'name': 'sessionid', [
'value': self.cookie.value, {
'domain': self.live_server_url.replace('http:', ''), "name": "sessionid",
'path': '/' "value": self.cookie.value,
}]) "domain": self.live_server_url.replace("http:", ""),
"path": "/",
}
]
)
return context return context
def open(self, url: str, playwright: Playwright) -> Page: def open(self, url: str, playwright: Playwright) -> Page:
browser = self.setup_browser(playwright) browser = self.setup_browser(playwright)
self.page = browser.new_page() self.page = browser.new_page()
self.page.goto(self.live_server_url + url) self.page.goto(self.live_server_url + url)
self.page.on('load', self.on_load) self.page.on("load", self.on_load)
self.num_loads = 0 self.num_loads = 0
return self.page return self.page
@ -35,20 +39,24 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
self.assertEqual(self.num_loads, count) self.assertEqual(self.num_loads, count)
def locate_bookmark(self, title: str): def locate_bookmark(self, title: str):
bookmark_tags = self.page.locator('li[ld-bookmark-item]') bookmark_tags = self.page.locator("li[ld-bookmark-item]")
return bookmark_tags.filter(has_text=title) return bookmark_tags.filter(has_text=title)
def locate_bulk_edit_bar(self): def locate_bulk_edit_bar(self):
return self.page.locator('.bulk-edit-bar') return self.page.locator(".bulk-edit-bar")
def locate_bulk_edit_select_all(self): def locate_bulk_edit_select_all(self):
return self.locate_bulk_edit_bar().locator('label[ld-bulk-edit-checkbox][all]') return self.locate_bulk_edit_bar().locator("label[ld-bulk-edit-checkbox][all]")
def locate_bulk_edit_select_across(self): def locate_bulk_edit_select_across(self):
return self.locate_bulk_edit_bar().locator('label.select-across') return self.locate_bulk_edit_bar().locator("label.select-across")
def locate_bulk_edit_toggle(self): def locate_bulk_edit_toggle(self):
return self.page.get_by_title('Bulk edit') return self.page.get_by_title("Bulk edit")
def select_bulk_action(self, value: str): def select_bulk_action(self, value: str):
return self.locate_bulk_edit_bar().locator('select[name="bulk_action"]').select_option(value) return (
self.locate_bulk_edit_bar()
.locator('select[name="bulk_action"]')
.select_option(value)
)

View file

@ -17,17 +17,21 @@ class FeedContext:
def sanitize(text: str): def sanitize(text: str):
if not text: if not text:
return '' return ""
# remove control characters # remove control characters
valid_chars = ['\n', '\r', '\t'] valid_chars = ["\n", "\r", "\t"]
return ''.join(ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != 'C') return "".join(
ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != "C"
)
class BaseBookmarksFeed(Feed): class BaseBookmarksFeed(Feed):
def get_object(self, request, feed_key: str): def get_object(self, request, feed_key: str):
feed_token = FeedToken.objects.get(key__exact=feed_key) feed_token = FeedToken.objects.get(key__exact=feed_key)
search = BookmarkSearch(q=request.GET.get('q', '')) search = BookmarkSearch(q=request.GET.get("q", ""))
query_set = queries.query_bookmarks(feed_token.user, feed_token.user.profile, search) query_set = queries.query_bookmarks(
feed_token.user, feed_token.user.profile, search
)
return FeedContext(feed_token, query_set) return FeedContext(feed_token, query_set)
def item_title(self, item: Bookmark): def item_title(self, item: Bookmark):
@ -44,22 +48,22 @@ class BaseBookmarksFeed(Feed):
class AllBookmarksFeed(BaseBookmarksFeed): class AllBookmarksFeed(BaseBookmarksFeed):
title = 'All bookmarks' title = "All bookmarks"
description = 'All bookmarks' description = "All bookmarks"
def link(self, context: FeedContext): def link(self, context: FeedContext):
return reverse('bookmarks:feeds.all', args=[context.feed_token.key]) return reverse("bookmarks:feeds.all", args=[context.feed_token.key])
def items(self, context: FeedContext): def items(self, context: FeedContext):
return context.query_set return context.query_set
class UnreadBookmarksFeed(BaseBookmarksFeed): class UnreadBookmarksFeed(BaseBookmarksFeed):
title = 'Unread bookmarks' title = "Unread bookmarks"
description = 'All unread bookmarks' description = "All unread bookmarks"
def link(self, context: FeedContext): def link(self, context: FeedContext):
return reverse('bookmarks:feeds.unread', args=[context.feed_token.key]) return reverse("bookmarks:feeds.unread", args=[context.feed_token.key])
def items(self, context: FeedContext): def items(self, context: FeedContext):
return context.query_set.filter(unread=True) return context.query_set.filter(unread=True)

View file

@ -8,19 +8,19 @@ class Command(BaseCommand):
help = "Creates a backup of the linkding database" help = "Creates a backup of the linkding database"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('destination', type=str, help='Backup file destination') parser.add_argument("destination", type=str, help="Backup file destination")
def handle(self, *args, **options): def handle(self, *args, **options):
destination = options['destination'] destination = options["destination"]
def progress(status, remaining, total): def progress(status, remaining, total):
self.stdout.write(f'Copied {total-remaining} of {total} pages...') self.stdout.write(f"Copied {total-remaining} of {total} pages...")
source_db = sqlite3.connect(os.path.join('data', 'db.sqlite3')) source_db = sqlite3.connect(os.path.join("data", "db.sqlite3"))
backup_db = sqlite3.connect(destination) backup_db = sqlite3.connect(destination)
with backup_db: with backup_db:
source_db.backup(backup_db, pages=50, progress=progress) source_db.backup(backup_db, pages=50, progress=progress)
backup_db.close() backup_db.close()
source_db.close() source_db.close()
self.stdout.write(self.style.SUCCESS(f'Backup created at {destination}')) self.stdout.write(self.style.SUCCESS(f"Backup created at {destination}"))

View file

@ -12,18 +12,20 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
User = get_user_model() User = get_user_model()
superuser_name = os.getenv('LD_SUPERUSER_NAME', None) superuser_name = os.getenv("LD_SUPERUSER_NAME", None)
superuser_password = os.getenv('LD_SUPERUSER_PASSWORD', None) superuser_password = os.getenv("LD_SUPERUSER_PASSWORD", None)
# Skip if option is undefined # Skip if option is undefined
if not superuser_name: if not superuser_name:
logger.info('Skip creating initial superuser, LD_SUPERUSER_NAME option is not defined') logger.info(
"Skip creating initial superuser, LD_SUPERUSER_NAME option is not defined"
)
return return
# Skip if user already exists # Skip if user already exists
user_exists = User.objects.filter(username=superuser_name).exists() user_exists = User.objects.filter(username=superuser_name).exists()
if user_exists: if user_exists:
logger.info('Skip creating initial superuser, user already exists') logger.info("Skip creating initial superuser, user already exists")
return return
user = User(username=superuser_name, is_superuser=True, is_staff=True) user = User(username=superuser_name, is_superuser=True, is_staff=True)
@ -34,4 +36,4 @@ class Command(BaseCommand):
user.set_unusable_password() user.set_unusable_password()
user.save() user.save()
logger.info('Created initial superuser') logger.info("Created initial superuser")

View file

@ -14,11 +14,11 @@ class Command(BaseCommand):
if not settings.USE_SQLITE: if not settings.USE_SQLITE:
return return
connection = connections['default'] connection = connections["default"]
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute("PRAGMA journal_mode") cursor.execute("PRAGMA journal_mode")
current_mode = cursor.fetchone()[0] current_mode = cursor.fetchone()[0]
logger.info(f'Current journal mode: {current_mode}') logger.info(f"Current journal mode: {current_mode}")
if current_mode != 'wal': if current_mode != "wal":
cursor.execute("PRAGMA journal_mode=wal;") cursor.execute("PRAGMA journal_mode=wal;")
logger.info('Switched to WAL journal mode') logger.info("Switched to WAL journal mode")

View file

@ -6,13 +6,15 @@ class Command(BaseCommand):
help = "Creates an admin user non-interactively if it doesn't exist" help = "Creates an admin user non-interactively if it doesn't exist"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('--username', help="Admin's username") parser.add_argument("--username", help="Admin's username")
parser.add_argument('--email', help="Admin's email") parser.add_argument("--email", help="Admin's email")
parser.add_argument('--password', help="Admin's password") parser.add_argument("--password", help="Admin's password")
def handle(self, *args, **options): def handle(self, *args, **options):
User = get_user_model() User = get_user_model()
if not User.objects.filter(username=options['username']).exists(): if not User.objects.filter(username=options["username"]).exists():
User.objects.create_superuser(username=options['username'], User.objects.create_superuser(
email=options['email'], username=options["username"],
password=options['password']) email=options["email"],
password=options["password"],
)

View file

@ -5,15 +5,17 @@ from bookmarks.services.importer import import_netscape_html
class Command(BaseCommand): class Command(BaseCommand):
help = 'Import Netscape HTML bookmark file' help = "Import Netscape HTML bookmark file"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('file', type=str, help='Path to file') parser.add_argument("file", type=str, help="Path to file")
parser.add_argument('user', type=str, help='Name of the user for which to import') parser.add_argument(
"user", type=str, help="Name of the user for which to import"
)
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
filepath = kwargs['file'] filepath = kwargs["file"]
username = kwargs['user'] username = kwargs["user"]
with open(filepath) as html_file: with open(filepath) as html_file:
html = html_file.read() html = html_file.read()
user = User.objects.get(username=username) user = User.objects.get(username=username)

View file

@ -15,19 +15,36 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Bookmark', name="Bookmark",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('url', models.URLField()), "id",
('title', models.CharField(max_length=512)), models.AutoField(
('description', models.TextField()), auto_created=True,
('website_title', models.CharField(blank=True, max_length=512, null=True)), primary_key=True,
('website_description', models.TextField(blank=True, null=True)), serialize=False,
('unread', models.BooleanField(default=True)), verbose_name="ID",
('date_added', models.DateTimeField()), ),
('date_modified', models.DateTimeField()), ),
('date_accessed', models.DateTimeField(blank=True, null=True)), ("url", models.URLField()),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ("title", models.CharField(max_length=512)),
("description", models.TextField()),
(
"website_title",
models.CharField(blank=True, max_length=512, null=True),
),
("website_description", models.TextField(blank=True, null=True)),
("unread", models.BooleanField(default=True)),
("date_added", models.DateTimeField()),
("date_modified", models.DateTimeField()),
("date_accessed", models.DateTimeField(blank=True, null=True)),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
], ],
), ),
] ]

View file

@ -9,22 +9,36 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('bookmarks', '0001_initial'), ("bookmarks", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Tag', name="Tag",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=64)), "id",
('date_added', models.DateTimeField()), models.AutoField(
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=64)),
("date_added", models.DateTimeField()),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='bookmark', model_name="bookmark",
name='tags', name="tags",
field=models.ManyToManyField(to='bookmarks.Tag'), field=models.ManyToManyField(to="bookmarks.Tag"),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0002_auto_20190629_2303'), ("bookmarks", "0002_auto_20190629_2303"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='bookmark', model_name="bookmark",
name='url', name="url",
field=models.URLField(max_length=2048), field=models.URLField(max_length=2048),
), ),
] ]

View file

@ -6,18 +6,18 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0003_auto_20200913_0656'), ("bookmarks", "0003_auto_20200913_0656"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='bookmark', model_name="bookmark",
name='description', name="description",
field=models.TextField(blank=True), field=models.TextField(blank=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='bookmark', model_name="bookmark",
name='title', name="title",
field=models.CharField(blank=True, max_length=512), field=models.CharField(blank=True, max_length=512),
), ),
] ]

View file

@ -7,13 +7,16 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0004_auto_20200926_1028'), ("bookmarks", "0004_auto_20200926_1028"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='bookmark', model_name="bookmark",
name='url', name="url",
field=models.CharField(max_length=2048, validators=[bookmarks.validators.BookmarkURLValidator()]), field=models.CharField(
max_length=2048,
validators=[bookmarks.validators.BookmarkURLValidator()],
),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0005_auto_20210103_1212'), ("bookmarks", "0005_auto_20210103_1212"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='bookmark', model_name="bookmark",
name='is_archived', name="is_archived",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
] ]

View file

@ -6,8 +6,8 @@ import django.db.models.deletion
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
User = apps.get_model('auth', 'User') User = apps.get_model("auth", "User")
UserProfile = apps.get_model('bookmarks', 'UserProfile') UserProfile = apps.get_model("bookmarks", "UserProfile")
for user in User.objects.all(): for user in User.objects.all():
try: try:
if user.profile: if user.profile:
@ -24,19 +24,42 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('bookmarks', '0006_bookmark_is_archived'), ("bookmarks", "0006_bookmark_is_archived"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='UserProfile', name="UserProfile",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('theme', "id",
models.CharField(choices=[('auto', 'Auto'), ('light', 'Light'), ('dark', 'Dark')], default='auto', models.AutoField(
max_length=10)), auto_created=True,
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', primary_key=True,
to=settings.AUTH_USER_MODEL)), serialize=False,
verbose_name="ID",
),
),
(
"theme",
models.CharField(
choices=[
("auto", "Auto"),
("light", "Light"),
("dark", "Dark"),
],
default="auto",
max_length=10,
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="profile",
to=settings.AUTH_USER_MODEL,
),
),
], ],
), ),
migrations.RunPython(forwards, reverse), migrations.RunPython(forwards, reverse),

View file

@ -6,13 +6,21 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0007_userprofile'), ("bookmarks", "0007_userprofile"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='bookmark_date_display', name="bookmark_date_display",
field=models.CharField(choices=[('relative', 'Relative'), ('absolute', 'Absolute'), ('hidden', 'Hidden')], default='relative', max_length=10), field=models.CharField(
choices=[
("relative", "Relative"),
("absolute", "Absolute"),
("hidden", "Hidden"),
],
default="relative",
max_length=10,
),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0008_userprofile_bookmark_date_display'), ("bookmarks", "0008_userprofile_bookmark_date_display"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='bookmark', model_name="bookmark",
name='web_archive_snapshot_url', name="web_archive_snapshot_url",
field=models.CharField(blank=True, max_length=2048), field=models.CharField(blank=True, max_length=2048),
), ),
] ]

View file

@ -6,13 +6,17 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0009_bookmark_web_archive_snapshot_url'), ("bookmarks", "0009_bookmark_web_archive_snapshot_url"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='bookmark_link_target', name="bookmark_link_target",
field=models.CharField(choices=[('_blank', 'New page'), ('_self', 'Same page')], default='_blank', max_length=10), field=models.CharField(
choices=[("_blank", "New page"), ("_self", "Same page")],
default="_blank",
max_length=10,
),
), ),
] ]

View file

@ -6,13 +6,17 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0010_userprofile_bookmark_link_target'), ("bookmarks", "0010_userprofile_bookmark_link_target"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='web_archive_integration', name="web_archive_integration",
field=models.CharField(choices=[('disabled', 'Disabled'), ('enabled', 'Enabled')], default='disabled', max_length=10), field=models.CharField(
choices=[("disabled", "Disabled"), ("enabled", "Enabled")],
default="disabled",
max_length=10,
),
), ),
] ]

View file

@ -9,18 +9,32 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('bookmarks', '0011_userprofile_web_archive_integration'), ("bookmarks", "0011_userprofile_web_archive_integration"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Toast', name="Toast",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('key', models.CharField(max_length=50)), "id",
('message', models.TextField()), models.AutoField(
('acknowledged', models.BooleanField(default=False)), auto_created=True,
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("key", models.CharField(max_length=50)),
("message", models.TextField()),
("acknowledged", models.BooleanField(default=False)),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
], ],
), ),
] ]

View file

@ -10,19 +10,21 @@ User = get_user_model()
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
for user in User.objects.all(): for user in User.objects.all():
toast = Toast(key='web_archive_opt_in_hint', toast = Toast(
message='The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.', key="web_archive_opt_in_hint",
owner=user) message="The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.",
owner=user,
)
toast.save() toast.save()
def reverse(apps, schema_editor): def reverse(apps, schema_editor):
Toast.objects.filter(key='web_archive_opt_in_hint').delete() Toast.objects.filter(key="web_archive_opt_in_hint").delete()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0012_toast'), ("bookmarks", "0012_toast"),
] ]
operations = [ operations = [

View file

@ -4,7 +4,7 @@ from django.db import migrations, models
def forwards(apps, schema_editor): def forwards(apps, schema_editor):
Bookmark = apps.get_model('bookmarks', 'Bookmark') Bookmark = apps.get_model("bookmarks", "Bookmark")
Bookmark.objects.update(unread=False) Bookmark.objects.update(unread=False)
@ -14,13 +14,13 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0013_web_archive_optin_toast'), ("bookmarks", "0013_web_archive_optin_toast"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='bookmark', model_name="bookmark",
name='unread', name="unread",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.RunPython(forwards, reverse), migrations.RunPython(forwards, reverse),

View file

@ -9,16 +9,26 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('bookmarks', '0014_alter_bookmark_unread'), ("bookmarks", "0014_alter_bookmark_unread"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='FeedToken', name="FeedToken",
fields=[ fields=[
('key', models.CharField(max_length=40, primary_key=True, serialize=False)), (
('created', models.DateTimeField(auto_now_add=True)), "key",
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feed_token', to=settings.AUTH_USER_MODEL)), models.CharField(max_length=40, primary_key=True, serialize=False),
),
("created", models.DateTimeField(auto_now_add=True)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="feed_token",
to=settings.AUTH_USER_MODEL,
),
),
], ],
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0015_feedtoken'), ("bookmarks", "0015_feedtoken"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='bookmark', model_name="bookmark",
name='shared', name="shared",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0016_bookmark_shared'), ("bookmarks", "0016_bookmark_shared"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='enable_sharing', name="enable_sharing",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0017_userprofile_enable_sharing'), ("bookmarks", "0017_userprofile_enable_sharing"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='bookmark', model_name="bookmark",
name='favicon_file', name="favicon_file",
field=models.CharField(blank=True, max_length=512), field=models.CharField(blank=True, max_length=512),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0018_bookmark_favicon_file'), ("bookmarks", "0018_bookmark_favicon_file"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='enable_favicons', name="enable_favicons",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
] ]

View file

@ -6,13 +6,17 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0019_userprofile_enable_favicons'), ("bookmarks", "0019_userprofile_enable_favicons"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='tag_search', name="tag_search",
field=models.CharField(choices=[('strict', 'Strict'), ('lax', 'Lax')], default='strict', max_length=10), field=models.CharField(
choices=[("strict", "Strict"), ("lax", "Lax")],
default="strict",
max_length=10,
),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0020_userprofile_tag_search'), ("bookmarks", "0020_userprofile_tag_search"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='display_url', name="display_url",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0021_userprofile_display_url'), ("bookmarks", "0021_userprofile_display_url"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='bookmark', model_name="bookmark",
name='notes', name="notes",
field=models.TextField(blank=True), field=models.TextField(blank=True),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0022_bookmark_notes'), ("bookmarks", "0022_bookmark_notes"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='permanent_notes', name="permanent_notes",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0023_userprofile_permanent_notes'), ("bookmarks", "0023_userprofile_permanent_notes"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='enable_public_sharing', name="enable_public_sharing",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('bookmarks', '0024_userprofile_enable_public_sharing'), ("bookmarks", "0024_userprofile_enable_public_sharing"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='userprofile', model_name="userprofile",
name='search_preferences', name="search_preferences",
field=models.JSONField(default=dict), field=models.JSONField(default=dict),
), ),
] ]

View file

@ -26,10 +26,10 @@ class Tag(models.Model):
def sanitize_tag_name(tag_name: str): def sanitize_tag_name(tag_name: str):
# strip leading/trailing spaces # strip leading/trailing spaces
# replace inner spaces with replacement char # replace inner spaces with replacement char
return tag_name.strip().replace(' ', '-') return tag_name.strip().replace(" ", "-")
def parse_tag_string(tag_string: str, delimiter: str = ','): def parse_tag_string(tag_string: str, delimiter: str = ","):
if not tag_string: if not tag_string:
return [] return []
names = tag_string.strip().split(delimiter) names = tag_string.strip().split(delimiter)
@ -42,7 +42,7 @@ def parse_tag_string(tag_string: str, delimiter: str = ','):
return names return names
def build_tag_string(tag_names: List[str], delimiter: str = ','): def build_tag_string(tag_names: List[str], delimiter: str = ","):
return delimiter.join(tag_names) return delimiter.join(tag_names)
@ -82,7 +82,7 @@ class Bookmark(models.Model):
return [tag.name for tag in self.tags.all()] return [tag.name for tag in self.tags.all()]
def __str__(self): def __str__(self):
return self.resolved_title + ' (' + self.url[:30] + '...)' return self.resolved_title + " (" + self.url[:30] + "...)"
class BookmarkForm(forms.ModelForm): class BookmarkForm(forms.ModelForm):
@ -90,15 +90,13 @@ class BookmarkForm(forms.ModelForm):
url = forms.CharField(validators=[BookmarkURLValidator()]) url = forms.CharField(validators=[BookmarkURLValidator()])
tag_string = forms.CharField(required=False) tag_string = forms.CharField(required=False)
# Do not require title and description in form as we fill these automatically if they are empty # Do not require title and description in form as we fill these automatically if they are empty
title = forms.CharField(max_length=512, title = forms.CharField(max_length=512, required=False)
required=False) description = forms.CharField(required=False, widget=forms.Textarea())
description = forms.CharField(required=False,
widget=forms.Textarea())
# Include website title and description as hidden field as they only provide info when editing bookmarks # Include website title and description as hidden field as they only provide info when editing bookmarks
website_title = forms.CharField(max_length=512, website_title = forms.CharField(
required=False, widget=forms.HiddenInput()) max_length=512, required=False, widget=forms.HiddenInput()
website_description = forms.CharField(required=False, )
widget=forms.HiddenInput()) website_description = forms.CharField(required=False, widget=forms.HiddenInput())
unread = forms.BooleanField(required=False) unread = forms.BooleanField(required=False)
shared = forms.BooleanField(required=False) shared = forms.BooleanField(required=False)
# Hidden field that determines whether to close window/tab after saving the bookmark # Hidden field that determines whether to close window/tab after saving the bookmark
@ -107,16 +105,16 @@ class BookmarkForm(forms.ModelForm):
class Meta: class Meta:
model = Bookmark model = Bookmark
fields = [ fields = [
'url', "url",
'tag_string', "tag_string",
'title', "title",
'description', "description",
'notes', "notes",
'website_title', "website_title",
'website_description', "website_description",
'unread', "unread",
'shared', "shared",
'auto_close', "auto_close",
] ]
@property @property
@ -125,45 +123,47 @@ class BookmarkForm(forms.ModelForm):
class BookmarkSearch: class BookmarkSearch:
SORT_ADDED_ASC = 'added_asc' SORT_ADDED_ASC = "added_asc"
SORT_ADDED_DESC = 'added_desc' SORT_ADDED_DESC = "added_desc"
SORT_TITLE_ASC = 'title_asc' SORT_TITLE_ASC = "title_asc"
SORT_TITLE_DESC = 'title_desc' SORT_TITLE_DESC = "title_desc"
FILTER_SHARED_OFF = 'off' FILTER_SHARED_OFF = "off"
FILTER_SHARED_SHARED = 'yes' FILTER_SHARED_SHARED = "yes"
FILTER_SHARED_UNSHARED = 'no' FILTER_SHARED_UNSHARED = "no"
FILTER_UNREAD_OFF = 'off' FILTER_UNREAD_OFF = "off"
FILTER_UNREAD_YES = 'yes' FILTER_UNREAD_YES = "yes"
FILTER_UNREAD_NO = 'no' FILTER_UNREAD_NO = "no"
params = ['q', 'user', 'sort', 'shared', 'unread'] params = ["q", "user", "sort", "shared", "unread"]
preferences = ['sort', 'shared', 'unread'] preferences = ["sort", "shared", "unread"]
defaults = { defaults = {
'q': '', "q": "",
'user': '', "user": "",
'sort': SORT_ADDED_DESC, "sort": SORT_ADDED_DESC,
'shared': FILTER_SHARED_OFF, "shared": FILTER_SHARED_OFF,
'unread': FILTER_UNREAD_OFF, "unread": FILTER_UNREAD_OFF,
} }
def __init__(self, def __init__(
self,
q: str = None, q: str = None,
user: str = None, user: str = None,
sort: str = None, sort: str = None,
shared: str = None, shared: str = None,
unread: str = None, unread: str = None,
preferences: dict = None): preferences: dict = None,
):
if not preferences: if not preferences:
preferences = {} preferences = {}
self.defaults = {**BookmarkSearch.defaults, **preferences} self.defaults = {**BookmarkSearch.defaults, **preferences}
self.q = q or self.defaults['q'] self.q = q or self.defaults["q"]
self.user = user or self.defaults['user'] self.user = user or self.defaults["user"]
self.sort = sort or self.defaults['sort'] self.sort = sort or self.defaults["sort"]
self.shared = shared or self.defaults['shared'] self.shared = shared or self.defaults["shared"]
self.unread = unread or self.defaults['unread'] self.unread = unread or self.defaults["unread"]
def is_modified(self, param): def is_modified(self, param):
value = self.__dict__[param] value = self.__dict__[param]
@ -175,7 +175,11 @@ class BookmarkSearch:
@property @property
def modified_preferences(self): def modified_preferences(self):
return [preference for preference in self.preferences if self.is_modified(preference)] return [
preference
for preference in self.preferences
if self.is_modified(preference)
]
@property @property
def has_modifications(self): def has_modifications(self):
@ -191,7 +195,9 @@ class BookmarkSearch:
@property @property
def preferences_dict(self): def preferences_dict(self):
return {preference: self.__dict__[preference] for preference in self.preferences} return {
preference: self.__dict__[preference] for preference in self.preferences
}
@staticmethod @staticmethod
def from_request(query_dict: QueryDict, preferences: dict = None): def from_request(query_dict: QueryDict, preferences: dict = None):
@ -206,20 +212,20 @@ class BookmarkSearch:
class BookmarkSearchForm(forms.Form): class BookmarkSearchForm(forms.Form):
SORT_CHOICES = [ SORT_CHOICES = [
(BookmarkSearch.SORT_ADDED_ASC, 'Added ↑'), (BookmarkSearch.SORT_ADDED_ASC, "Added ↑"),
(BookmarkSearch.SORT_ADDED_DESC, 'Added ↓'), (BookmarkSearch.SORT_ADDED_DESC, "Added ↓"),
(BookmarkSearch.SORT_TITLE_ASC, 'Title ↑'), (BookmarkSearch.SORT_TITLE_ASC, "Title ↑"),
(BookmarkSearch.SORT_TITLE_DESC, 'Title ↓'), (BookmarkSearch.SORT_TITLE_DESC, "Title ↓"),
] ]
FILTER_SHARED_CHOICES = [ FILTER_SHARED_CHOICES = [
(BookmarkSearch.FILTER_SHARED_OFF, 'Off'), (BookmarkSearch.FILTER_SHARED_OFF, "Off"),
(BookmarkSearch.FILTER_SHARED_SHARED, 'Shared'), (BookmarkSearch.FILTER_SHARED_SHARED, "Shared"),
(BookmarkSearch.FILTER_SHARED_UNSHARED, 'Unshared'), (BookmarkSearch.FILTER_SHARED_UNSHARED, "Unshared"),
] ]
FILTER_UNREAD_CHOICES = [ FILTER_UNREAD_CHOICES = [
(BookmarkSearch.FILTER_UNREAD_OFF, 'Off'), (BookmarkSearch.FILTER_UNREAD_OFF, "Off"),
(BookmarkSearch.FILTER_UNREAD_YES, 'Unread'), (BookmarkSearch.FILTER_UNREAD_YES, "Unread"),
(BookmarkSearch.FILTER_UNREAD_NO, 'Read'), (BookmarkSearch.FILTER_UNREAD_NO, "Read"),
] ]
q = forms.CharField() q = forms.CharField()
@ -228,7 +234,12 @@ class BookmarkSearchForm(forms.Form):
shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect) shared = forms.ChoiceField(choices=FILTER_SHARED_CHOICES, widget=forms.RadioSelect)
unread = forms.ChoiceField(choices=FILTER_UNREAD_CHOICES, widget=forms.RadioSelect) unread = forms.ChoiceField(choices=FILTER_UNREAD_CHOICES, widget=forms.RadioSelect)
def __init__(self, search: BookmarkSearch, editable_fields: List[str] = None, users: List[User] = None): def __init__(
self,
search: BookmarkSearch,
editable_fields: List[str] = None,
users: List[User] = None,
):
super().__init__() super().__init__()
editable_fields = editable_fields or [] editable_fields = editable_fields or []
self.editable_fields = editable_fields self.editable_fields = editable_fields
@ -236,8 +247,8 @@ class BookmarkSearchForm(forms.Form):
# set choices for user field if users are provided # set choices for user field if users are provided
if users: if users:
user_choices = [(user.username, user.username) for user in users] user_choices = [(user.username, user.username) for user in users]
user_choices.insert(0, ('', 'Everyone')) user_choices.insert(0, ("", "Everyone"))
self.fields['user'].choices = user_choices self.fields["user"].choices = user_choices
for param in search.params: for param in search.params:
# set initial values for modified params # set initial values for modified params
@ -251,50 +262,70 @@ class BookmarkSearchForm(forms.Form):
class UserProfile(models.Model): class UserProfile(models.Model):
THEME_AUTO = 'auto' THEME_AUTO = "auto"
THEME_LIGHT = 'light' THEME_LIGHT = "light"
THEME_DARK = 'dark' THEME_DARK = "dark"
THEME_CHOICES = [ THEME_CHOICES = [
(THEME_AUTO, 'Auto'), (THEME_AUTO, "Auto"),
(THEME_LIGHT, 'Light'), (THEME_LIGHT, "Light"),
(THEME_DARK, 'Dark'), (THEME_DARK, "Dark"),
] ]
BOOKMARK_DATE_DISPLAY_RELATIVE = 'relative' BOOKMARK_DATE_DISPLAY_RELATIVE = "relative"
BOOKMARK_DATE_DISPLAY_ABSOLUTE = 'absolute' BOOKMARK_DATE_DISPLAY_ABSOLUTE = "absolute"
BOOKMARK_DATE_DISPLAY_HIDDEN = 'hidden' BOOKMARK_DATE_DISPLAY_HIDDEN = "hidden"
BOOKMARK_DATE_DISPLAY_CHOICES = [ BOOKMARK_DATE_DISPLAY_CHOICES = [
(BOOKMARK_DATE_DISPLAY_RELATIVE, 'Relative'), (BOOKMARK_DATE_DISPLAY_RELATIVE, "Relative"),
(BOOKMARK_DATE_DISPLAY_ABSOLUTE, 'Absolute'), (BOOKMARK_DATE_DISPLAY_ABSOLUTE, "Absolute"),
(BOOKMARK_DATE_DISPLAY_HIDDEN, 'Hidden'), (BOOKMARK_DATE_DISPLAY_HIDDEN, "Hidden"),
] ]
BOOKMARK_LINK_TARGET_BLANK = '_blank' BOOKMARK_LINK_TARGET_BLANK = "_blank"
BOOKMARK_LINK_TARGET_SELF = '_self' BOOKMARK_LINK_TARGET_SELF = "_self"
BOOKMARK_LINK_TARGET_CHOICES = [ BOOKMARK_LINK_TARGET_CHOICES = [
(BOOKMARK_LINK_TARGET_BLANK, 'New page'), (BOOKMARK_LINK_TARGET_BLANK, "New page"),
(BOOKMARK_LINK_TARGET_SELF, 'Same page'), (BOOKMARK_LINK_TARGET_SELF, "Same page"),
] ]
WEB_ARCHIVE_INTEGRATION_DISABLED = 'disabled' WEB_ARCHIVE_INTEGRATION_DISABLED = "disabled"
WEB_ARCHIVE_INTEGRATION_ENABLED = 'enabled' WEB_ARCHIVE_INTEGRATION_ENABLED = "enabled"
WEB_ARCHIVE_INTEGRATION_CHOICES = [ WEB_ARCHIVE_INTEGRATION_CHOICES = [
(WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'), (WEB_ARCHIVE_INTEGRATION_DISABLED, "Disabled"),
(WEB_ARCHIVE_INTEGRATION_ENABLED, 'Enabled'), (WEB_ARCHIVE_INTEGRATION_ENABLED, "Enabled"),
] ]
TAG_SEARCH_STRICT = 'strict' TAG_SEARCH_STRICT = "strict"
TAG_SEARCH_LAX = 'lax' TAG_SEARCH_LAX = "lax"
TAG_SEARCH_CHOICES = [ TAG_SEARCH_CHOICES = [
(TAG_SEARCH_STRICT, 'Strict'), (TAG_SEARCH_STRICT, "Strict"),
(TAG_SEARCH_LAX, 'Lax'), (TAG_SEARCH_LAX, "Lax"),
] ]
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE) user = models.OneToOneField(
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO) get_user_model(), related_name="profile", on_delete=models.CASCADE
bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, blank=False, )
default=BOOKMARK_DATE_DISPLAY_RELATIVE) theme = models.CharField(
bookmark_link_target = models.CharField(max_length=10, choices=BOOKMARK_LINK_TARGET_CHOICES, blank=False, max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO
default=BOOKMARK_LINK_TARGET_BLANK) )
web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False, bookmark_date_display = models.CharField(
default=WEB_ARCHIVE_INTEGRATION_DISABLED) max_length=10,
tag_search = models.CharField(max_length=10, choices=TAG_SEARCH_CHOICES, blank=False, choices=BOOKMARK_DATE_DISPLAY_CHOICES,
default=TAG_SEARCH_STRICT) blank=False,
default=BOOKMARK_DATE_DISPLAY_RELATIVE,
)
bookmark_link_target = models.CharField(
max_length=10,
choices=BOOKMARK_LINK_TARGET_CHOICES,
blank=False,
default=BOOKMARK_LINK_TARGET_BLANK,
)
web_archive_integration = models.CharField(
max_length=10,
choices=WEB_ARCHIVE_INTEGRATION_CHOICES,
blank=False,
default=WEB_ARCHIVE_INTEGRATION_DISABLED,
)
tag_search = models.CharField(
max_length=10,
choices=TAG_SEARCH_CHOICES,
blank=False,
default=TAG_SEARCH_STRICT,
)
enable_sharing = models.BooleanField(default=False, null=False) enable_sharing = models.BooleanField(default=False, null=False)
enable_public_sharing = models.BooleanField(default=False, null=False) enable_public_sharing = models.BooleanField(default=False, null=False)
enable_favicons = models.BooleanField(default=False, null=False) enable_favicons = models.BooleanField(default=False, null=False)
@ -306,8 +337,18 @@ class UserProfile(models.Model):
class UserProfileForm(forms.ModelForm): class UserProfileForm(forms.ModelForm):
class Meta: class Meta:
model = UserProfile model = UserProfile
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'tag_search', fields = [
'enable_sharing', 'enable_public_sharing', 'enable_favicons', 'display_url', 'permanent_notes'] "theme",
"bookmark_date_display",
"bookmark_link_target",
"web_archive_integration",
"tag_search",
"enable_sharing",
"enable_public_sharing",
"enable_favicons",
"display_url",
"permanent_notes",
]
@receiver(post_save, sender=get_user_model()) @receiver(post_save, sender=get_user_model())
@ -332,9 +373,11 @@ class FeedToken(models.Model):
""" """
Adapted from authtoken.models.Token Adapted from authtoken.models.Token
""" """
key = models.CharField(max_length=40, primary_key=True) key = models.CharField(max_length=40, primary_key=True)
user = models.OneToOneField(get_user_model(), user = models.OneToOneField(
related_name='feed_token', get_user_model(),
related_name="feed_token",
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)

View file

@ -10,18 +10,24 @@ from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
from bookmarks.utils import unique from bookmarks.utils import unique
def query_bookmarks(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet: def query_bookmarks(
return _base_bookmarks_query(user, profile, search) \ user: User, profile: UserProfile, search: BookmarkSearch
.filter(is_archived=False) ) -> QuerySet:
return _base_bookmarks_query(user, profile, search).filter(is_archived=False)
def query_archived_bookmarks(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet: def query_archived_bookmarks(
return _base_bookmarks_query(user, profile, search) \ user: User, profile: UserProfile, search: BookmarkSearch
.filter(is_archived=True) ) -> QuerySet:
return _base_bookmarks_query(user, profile, search).filter(is_archived=True)
def query_shared_bookmarks(user: Optional[User], profile: UserProfile, search: BookmarkSearch, def query_shared_bookmarks(
public_only: bool) -> QuerySet: user: Optional[User],
profile: UserProfile,
search: BookmarkSearch,
public_only: bool,
) -> QuerySet:
conditions = Q(shared=True) & Q(owner__profile__enable_sharing=True) conditions = Q(shared=True) & Q(owner__profile__enable_sharing=True)
if public_only: if public_only:
conditions = conditions & Q(owner__profile__enable_public_sharing=True) conditions = conditions & Q(owner__profile__enable_public_sharing=True)
@ -29,7 +35,9 @@ def query_shared_bookmarks(user: Optional[User], profile: UserProfile, search: B
return _base_bookmarks_query(user, profile, search).filter(conditions) return _base_bookmarks_query(user, profile, search).filter(conditions)
def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: BookmarkSearch) -> QuerySet: def _base_bookmarks_query(
user: Optional[User], profile: UserProfile, search: BookmarkSearch
) -> QuerySet:
query_set = Bookmark.objects query_set = Bookmark.objects
# Filter for user # Filter for user
@ -40,34 +48,32 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
query = parse_query_string(search.q) query = parse_query_string(search.q)
# Filter for search terms and tags # Filter for search terms and tags
for term in query['search_terms']: for term in query["search_terms"]:
conditions = Q(title__icontains=term) \ conditions = (
| Q(description__icontains=term) \ Q(title__icontains=term)
| Q(notes__icontains=term) \ | Q(description__icontains=term)
| Q(website_title__icontains=term) \ | Q(notes__icontains=term)
| Q(website_description__icontains=term) \ | Q(website_title__icontains=term)
| Q(website_description__icontains=term)
| Q(url__icontains=term) | Q(url__icontains=term)
)
if profile.tag_search == UserProfile.TAG_SEARCH_LAX: if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
conditions = conditions | Exists(Bookmark.objects.filter(id=OuterRef('id'), tags__name__iexact=term)) conditions = conditions | Exists(
Bookmark.objects.filter(id=OuterRef("id"), tags__name__iexact=term)
)
query_set = query_set.filter(conditions) query_set = query_set.filter(conditions)
for tag_name in query['tag_names']: for tag_name in query["tag_names"]:
query_set = query_set.filter( query_set = query_set.filter(tags__name__iexact=tag_name)
tags__name__iexact=tag_name
)
# Untagged bookmarks # Untagged bookmarks
if query['untagged']: if query["untagged"]:
query_set = query_set.filter( query_set = query_set.filter(tags=None)
tags=None
)
# Legacy unread bookmarks filter from query # Legacy unread bookmarks filter from query
if query['unread']: if query["unread"]:
query_set = query_set.filter( query_set = query_set.filter(unread=True)
unread=True
)
# Unread filter from bookmark search # Unread filter from bookmark search
if search.unread == BookmarkSearch.FILTER_UNREAD_YES: if search.unread == BookmarkSearch.FILTER_UNREAD_YES:
@ -83,29 +89,36 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
# Sort by date added # Sort by date added
if search.sort == BookmarkSearch.SORT_ADDED_ASC: if search.sort == BookmarkSearch.SORT_ADDED_ASC:
query_set = query_set.order_by('date_added') query_set = query_set.order_by("date_added")
elif search.sort == BookmarkSearch.SORT_ADDED_DESC: elif search.sort == BookmarkSearch.SORT_ADDED_DESC:
query_set = query_set.order_by('-date_added') query_set = query_set.order_by("-date_added")
# Sort by title # Sort by title
if search.sort == BookmarkSearch.SORT_TITLE_ASC or search.sort == BookmarkSearch.SORT_TITLE_DESC: if (
search.sort == BookmarkSearch.SORT_TITLE_ASC
or search.sort == BookmarkSearch.SORT_TITLE_DESC
):
# For the title, the resolved_title logic from the Bookmark entity needs # For the title, the resolved_title logic from the Bookmark entity needs
# to be replicated as there is no corresponding database field # to be replicated as there is no corresponding database field
query_set = query_set.annotate( query_set = query_set.annotate(
effective_title=Case( effective_title=Case(
When(Q(title__isnull=False) & ~Q(title__exact=''), then=Lower('title')), When(Q(title__isnull=False) & ~Q(title__exact=""), then=Lower("title")),
When(Q(website_title__isnull=False) & ~Q(website_title__exact=''), then=Lower('website_title')), When(
default=Lower('url'), Q(website_title__isnull=False) & ~Q(website_title__exact=""),
output_field=CharField() then=Lower("website_title"),
)) ),
default=Lower("url"),
output_field=CharField(),
)
)
# For SQLite, if the ICU extension is loaded, use the custom collation # For SQLite, if the ICU extension is loaded, use the custom collation
# loaded into the connection. This results in an improved sort order for # loaded into the connection. This results in an improved sort order for
# unicode characters (umlauts, etc.) # unicode characters (umlauts, etc.)
if settings.USE_SQLITE and settings.USE_SQLITE_ICU_EXTENSION: if settings.USE_SQLITE and settings.USE_SQLITE_ICU_EXTENSION:
order_field = RawSQL('effective_title COLLATE ICU', ()) order_field = RawSQL("effective_title COLLATE ICU", ())
else: else:
order_field = 'effective_title' order_field = "effective_title"
if search.sort == BookmarkSearch.SORT_TITLE_ASC: if search.sort == BookmarkSearch.SORT_TITLE_ASC:
query_set = query_set.order_by(order_field) query_set = query_set.order_by(order_field)
@ -115,7 +128,9 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
return query_set return query_set
def query_bookmark_tags(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet: def query_bookmark_tags(
user: User, profile: UserProfile, search: BookmarkSearch
) -> QuerySet:
bookmarks_query = query_bookmarks(user, profile, search) bookmarks_query = query_bookmarks(user, profile, search)
query_set = Tag.objects.filter(bookmark__in=bookmarks_query) query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
@ -123,7 +138,9 @@ def query_bookmark_tags(user: User, profile: UserProfile, search: BookmarkSearch
return query_set.distinct() return query_set.distinct()
def query_archived_bookmark_tags(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet: def query_archived_bookmark_tags(
user: User, profile: UserProfile, search: BookmarkSearch
) -> QuerySet:
bookmarks_query = query_archived_bookmarks(user, profile, search) bookmarks_query = query_archived_bookmarks(user, profile, search)
query_set = Tag.objects.filter(bookmark__in=bookmarks_query) query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
@ -131,8 +148,12 @@ def query_archived_bookmark_tags(user: User, profile: UserProfile, search: Bookm
return query_set.distinct() return query_set.distinct()
def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, search: BookmarkSearch, def query_shared_bookmark_tags(
public_only: bool) -> QuerySet: user: Optional[User],
profile: UserProfile,
search: BookmarkSearch,
public_only: bool,
) -> QuerySet:
bookmarks_query = query_shared_bookmarks(user, profile, search, public_only) bookmarks_query = query_shared_bookmarks(user, profile, search, public_only)
query_set = Tag.objects.filter(bookmark__in=bookmarks_query) query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
@ -140,7 +161,9 @@ def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, searc
return query_set.distinct() return query_set.distinct()
def query_shared_bookmark_users(profile: UserProfile, search: BookmarkSearch, public_only: bool) -> QuerySet: def query_shared_bookmark_users(
profile: UserProfile, search: BookmarkSearch, public_only: bool
) -> QuerySet:
bookmarks_query = query_shared_bookmarks(None, profile, search, public_only) bookmarks_query = query_shared_bookmarks(None, profile, search, public_only)
query_set = User.objects.filter(bookmark__in=bookmarks_query) query_set = User.objects.filter(bookmark__in=bookmarks_query)
@ -155,23 +178,23 @@ def get_user_tags(user: User):
def parse_query_string(query_string): def parse_query_string(query_string):
# Sanitize query params # Sanitize query params
if not query_string: if not query_string:
query_string = '' query_string = ""
# Split query into search terms and tags # Split query into search terms and tags
keywords = query_string.strip().split(' ') keywords = query_string.strip().split(" ")
keywords = [word for word in keywords if word] keywords = [word for word in keywords if word]
search_terms = [word for word in keywords if word[0] != '#' and word[0] != '!'] search_terms = [word for word in keywords if word[0] != "#" and word[0] != "!"]
tag_names = [word[1:] for word in keywords if word[0] == '#'] tag_names = [word[1:] for word in keywords if word[0] == "#"]
tag_names = unique(tag_names, str.lower) tag_names = unique(tag_names, str.lower)
# Special search commands # Special search commands
untagged = '!untagged' in keywords untagged = "!untagged" in keywords
unread = '!unread' in keywords unread = "!unread" in keywords
return { return {
'search_terms': search_terms, "search_terms": search_terms,
'tag_names': tag_names, "tag_names": tag_names,
'untagged': untagged, "untagged": untagged,
'unread': unread, "unread": unread,
} }

View file

@ -11,7 +11,9 @@ from bookmarks.services import tasks
def create_bookmark(bookmark: Bookmark, tag_string: str, current_user: User): def create_bookmark(bookmark: Bookmark, tag_string: str, current_user: User):
# If URL is already bookmarked, then update it # If URL is already bookmarked, then update it
existing_bookmark: Bookmark = Bookmark.objects.filter(owner=current_user, url=bookmark.url).first() existing_bookmark: Bookmark = Bookmark.objects.filter(
owner=current_user, url=bookmark.url
).first()
if existing_bookmark is not None: if existing_bookmark is not None:
_merge_bookmark_data(bookmark, existing_bookmark) _merge_bookmark_data(bookmark, existing_bookmark)
@ -68,8 +70,9 @@ def archive_bookmark(bookmark: Bookmark):
def archive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def archive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(is_archived=True, Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
date_modified=timezone.now()) is_archived=True, date_modified=timezone.now()
)
def unarchive_bookmark(bookmark: Bookmark): def unarchive_bookmark(bookmark: Bookmark):
@ -82,8 +85,9 @@ def unarchive_bookmark(bookmark: Bookmark):
def unarchive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def unarchive_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(is_archived=False, Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
date_modified=timezone.now()) is_archived=False, date_modified=timezone.now()
)
def delete_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def delete_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
@ -94,8 +98,9 @@ def delete_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User): def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
owned_bookmark_ids = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).values_list('id', owned_bookmark_ids = Bookmark.objects.filter(
flat=True) owner=current_user, id__in=sanitized_bookmark_ids
).values_list("id", flat=True)
tag_names = parse_tag_string(tag_string) tag_names = parse_tag_string(tag_string)
tags = get_or_create_tags(tag_names, current_user) tags = get_or_create_tags(tag_names, current_user)
@ -103,54 +108,69 @@ def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user
relationships = [] relationships = []
for tag in tags: for tag in tags:
for bookmark_id in owned_bookmark_ids: for bookmark_id in owned_bookmark_ids:
relationships.append(BookmarkToTagRelationShip(bookmark_id=bookmark_id, tag=tag)) relationships.append(
BookmarkToTagRelationShip(bookmark_id=bookmark_id, tag=tag)
)
# Insert all bookmark -> tag associations at once, should ignore errors if association already exists # Insert all bookmark -> tag associations at once, should ignore errors if association already exists
BookmarkToTagRelationShip.objects.bulk_create(relationships, ignore_conflicts=True) BookmarkToTagRelationShip.objects.bulk_create(relationships, ignore_conflicts=True)
Bookmark.objects.filter(id__in=owned_bookmark_ids).update(date_modified=timezone.now()) Bookmark.objects.filter(id__in=owned_bookmark_ids).update(
date_modified=timezone.now()
)
def untag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User): def untag_bookmarks(
bookmark_ids: [Union[int, str]], tag_string: str, current_user: User
):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
owned_bookmark_ids = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).values_list('id', owned_bookmark_ids = Bookmark.objects.filter(
flat=True) owner=current_user, id__in=sanitized_bookmark_ids
).values_list("id", flat=True)
tag_names = parse_tag_string(tag_string) tag_names = parse_tag_string(tag_string)
tags = get_or_create_tags(tag_names, current_user) tags = get_or_create_tags(tag_names, current_user)
BookmarkToTagRelationShip = Bookmark.tags.through BookmarkToTagRelationShip = Bookmark.tags.through
for tag in tags: for tag in tags:
# Remove all bookmark -> tag associations for the owned bookmarks and the current tag # Remove all bookmark -> tag associations for the owned bookmarks and the current tag
BookmarkToTagRelationShip.objects.filter(bookmark_id__in=owned_bookmark_ids, tag=tag).delete() BookmarkToTagRelationShip.objects.filter(
bookmark_id__in=owned_bookmark_ids, tag=tag
).delete()
Bookmark.objects.filter(id__in=owned_bookmark_ids).update(date_modified=timezone.now()) Bookmark.objects.filter(id__in=owned_bookmark_ids).update(
date_modified=timezone.now()
)
def mark_bookmarks_as_read(bookmark_ids: [Union[int, str]], current_user: User): def mark_bookmarks_as_read(bookmark_ids: [Union[int, str]], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(unread=False, Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
date_modified=timezone.now()) unread=False, date_modified=timezone.now()
)
def mark_bookmarks_as_unread(bookmark_ids: [Union[int, str]], current_user: User): def mark_bookmarks_as_unread(bookmark_ids: [Union[int, str]], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(unread=True, Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
date_modified=timezone.now()) unread=True, date_modified=timezone.now()
)
def share_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def share_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(shared=True, Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
date_modified=timezone.now()) shared=True, date_modified=timezone.now()
)
def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(shared=False, Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(
date_modified=timezone.now()) shared=False, date_modified=timezone.now()
)
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark): def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):

View file

@ -13,40 +13,41 @@ def export_netscape_html(bookmarks: List[Bookmark]):
[append_bookmark(doc, bookmark) for bookmark in bookmarks] [append_bookmark(doc, bookmark) for bookmark in bookmarks]
append_list_end(doc) append_list_end(doc)
return '\n\r'.join(doc) return "\n\r".join(doc)
def append_header(doc: BookmarkDocument): def append_header(doc: BookmarkDocument):
doc.append('<!DOCTYPE NETSCAPE-Bookmark-file-1>') doc.append("<!DOCTYPE NETSCAPE-Bookmark-file-1>")
doc.append('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">') doc.append('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">')
doc.append('<TITLE>Bookmarks</TITLE>') doc.append("<TITLE>Bookmarks</TITLE>")
doc.append('<H1>Bookmarks</H1>') doc.append("<H1>Bookmarks</H1>")
def append_list_start(doc: BookmarkDocument): def append_list_start(doc: BookmarkDocument):
doc.append('<DL><p>') doc.append("<DL><p>")
def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark): def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
url = bookmark.url url = bookmark.url
title = html.escape(bookmark.resolved_title or '') title = html.escape(bookmark.resolved_title or "")
desc = html.escape(bookmark.resolved_description or '') desc = html.escape(bookmark.resolved_description or "")
if bookmark.notes: if bookmark.notes:
desc += f'[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]' desc += f"[linkding-notes]{html.escape(bookmark.notes)}[/linkding-notes]"
tag_names = bookmark.tag_names tag_names = bookmark.tag_names
if bookmark.is_archived: if bookmark.is_archived:
tag_names.append('linkding:archived') tag_names.append("linkding:archived")
tags = ','.join(tag_names) tags = ",".join(tag_names)
toread = '1' if bookmark.unread else '0' toread = "1" if bookmark.unread else "0"
private = '0' if bookmark.shared else '1' private = "0" if bookmark.shared else "1"
added = int(bookmark.date_added.timestamp()) added = int(bookmark.date_added.timestamp())
doc.append( doc.append(
f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>') f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>'
)
if desc: if desc:
doc.append(f'<DD>{desc}') doc.append(f"<DD>{desc}")
def append_list_end(doc: BookmarkDocument): def append_list_end(doc: BookmarkDocument):
doc.append('</DL><p>') doc.append("</DL><p>")

View file

@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
# register mime type for .ico files, which is not included in the default # register mime type for .ico files, which is not included in the default
# mimetypes of the Docker image # mimetypes of the Docker image
mimetypes.add_type('image/x-icon', '.ico') mimetypes.add_type("image/x-icon", ".ico")
def _ensure_favicon_folder(): def _ensure_favicon_folder():
@ -23,16 +23,16 @@ def _ensure_favicon_folder():
def _url_to_filename(url: str) -> str: def _url_to_filename(url: str) -> str:
return re.sub(r'\W+', '_', url) return re.sub(r"\W+", "_", url)
def _get_url_parameters(url: str) -> dict: def _get_url_parameters(url: str) -> dict:
parsed_uri = urlparse(url) parsed_uri = urlparse(url)
return { return {
# https://example.com/foo?bar -> https://example.com # https://example.com/foo?bar -> https://example.com
'url': f'{parsed_uri.scheme}://{parsed_uri.hostname}', "url": f"{parsed_uri.scheme}://{parsed_uri.hostname}",
# https://example.com/foo?bar -> example.com # https://example.com/foo?bar -> example.com
'domain': parsed_uri.hostname, "domain": parsed_uri.hostname,
} }
@ -63,21 +63,21 @@ def load_favicon(url: str) -> str:
# Create favicon folder if not exists # Create favicon folder if not exists
_ensure_favicon_folder() _ensure_favicon_folder()
# Use scheme+hostname as favicon filename to reuse icon for all pages on the same domain # Use scheme+hostname as favicon filename to reuse icon for all pages on the same domain
favicon_name = _url_to_filename(url_parameters['url']) favicon_name = _url_to_filename(url_parameters["url"])
favicon_file = _check_existing_favicon(favicon_name) favicon_file = _check_existing_favicon(favicon_name)
if not favicon_file: if not favicon_file:
# Load favicon from provider, save to file # Load favicon from provider, save to file
favicon_url = settings.LD_FAVICON_PROVIDER.format(**url_parameters) favicon_url = settings.LD_FAVICON_PROVIDER.format(**url_parameters)
logger.debug(f'Loading favicon from: {favicon_url}') logger.debug(f"Loading favicon from: {favicon_url}")
with requests.get(favicon_url, stream=True) as response: with requests.get(favicon_url, stream=True) as response:
content_type = response.headers['Content-Type'] content_type = response.headers["Content-Type"]
file_extension = mimetypes.guess_extension(content_type) file_extension = mimetypes.guess_extension(content_type)
favicon_file = f'{favicon_name}{file_extension}' favicon_file = f"{favicon_name}{file_extension}"
favicon_path = _get_favicon_path(favicon_file) favicon_path = _get_favicon_path(favicon_file)
with open(favicon_path, 'wb') as file: with open(favicon_path, "wb") as file:
for chunk in response.iter_content(chunk_size=8192): for chunk in response.iter_content(chunk_size=8192):
file.write(chunk) file.write(chunk)
logger.debug(f'Saved favicon as: {favicon_path}') logger.debug(f"Saved favicon as: {favicon_path}")
return favicon_file return favicon_file

View file

@ -55,18 +55,20 @@ class TagCache:
self.cache[tag.name.lower()] = tag self.cache[tag.name.lower()] = tag
def import_netscape_html(html: str, user: User, options: ImportOptions = ImportOptions()) -> ImportResult: def import_netscape_html(
html: str, user: User, options: ImportOptions = ImportOptions()
) -> ImportResult:
result = ImportResult() result = ImportResult()
import_start = timezone.now() import_start = timezone.now()
try: try:
netscape_bookmarks = parse(html) netscape_bookmarks = parse(html)
except: except:
logging.exception('Could not read bookmarks file.') logging.exception("Could not read bookmarks file.")
raise raise
parse_end = timezone.now() parse_end = timezone.now()
logger.debug(f'Parse duration: {parse_end - import_start}') logger.debug(f"Parse duration: {parse_end - import_start}")
# Create and cache all tags beforehand # Create and cache all tags beforehand
_create_missing_tags(netscape_bookmarks, user) _create_missing_tags(netscape_bookmarks, user)
@ -83,7 +85,7 @@ def import_netscape_html(html: str, user: User, options: ImportOptions = ImportO
tasks.schedule_bookmarks_without_favicons(user) tasks.schedule_bookmarks_without_favicons(user)
end = timezone.now() end = timezone.now()
logger.debug(f'Import duration: {end - import_start}') logger.debug(f"Import duration: {end - import_start}")
return result return result
@ -110,7 +112,7 @@ def _get_batches(items: List, batch_size: int):
num_items = len(items) num_items = len(items)
while offset < num_items: while offset < num_items:
batch = items[offset:min(offset + batch_size, num_items)] batch = items[offset : min(offset + batch_size, num_items)]
if len(batch) > 0: if len(batch) > 0:
batches.append(batch) batches.append(batch)
offset = offset + batch_size offset = offset + batch_size
@ -118,11 +120,13 @@ def _get_batches(items: List, batch_size: int):
return batches return batches
def _import_batch(netscape_bookmarks: List[NetscapeBookmark], def _import_batch(
netscape_bookmarks: List[NetscapeBookmark],
user: User, user: User,
options: ImportOptions, options: ImportOptions,
tag_cache: TagCache, tag_cache: TagCache,
result: ImportResult): result: ImportResult,
):
# Query existing bookmarks # Query existing bookmarks
batch_urls = [bookmark.href for bookmark in netscape_bookmarks] batch_urls = [bookmark.href for bookmark in netscape_bookmarks]
existing_bookmarks = Bookmark.objects.filter(owner=user, url__in=batch_urls) existing_bookmarks = Bookmark.objects.filter(owner=user, url__in=batch_urls)
@ -136,7 +140,13 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
try: try:
# Lookup existing bookmark by URL, or create new bookmark if there is no bookmark for that URL yet # Lookup existing bookmark by URL, or create new bookmark if there is no bookmark for that URL yet
bookmark = next( bookmark = next(
(bookmark for bookmark in existing_bookmarks if bookmark.url == netscape_bookmark.href), None) (
bookmark
for bookmark in existing_bookmarks
if bookmark.url == netscape_bookmark.href
),
None,
)
if not bookmark: if not bookmark:
bookmark = Bookmark(owner=user) bookmark = Bookmark(owner=user)
is_update = False is_update = False
@ -146,7 +156,7 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
_copy_bookmark_data(netscape_bookmark, bookmark, options) _copy_bookmark_data(netscape_bookmark, bookmark, options)
# Validate bookmark fields, exclude owner to prevent n+1 database query, # Validate bookmark fields, exclude owner to prevent n+1 database query,
# also there is no specific validation on owner # also there is no specific validation on owner
bookmark.clean_fields(exclude=['owner']) bookmark.clean_fields(exclude=["owner"])
# Schedule for update or insert # Schedule for update or insert
if is_update: if is_update:
bookmarks_to_update.append(bookmark) bookmarks_to_update.append(bookmark)
@ -155,20 +165,25 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
result.success = result.success + 1 result.success = result.success + 1
except: except:
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + '...' shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + "..."
logging.exception('Error importing bookmark: ' + shortened_bookmark_tag_str) logging.exception("Error importing bookmark: " + shortened_bookmark_tag_str)
result.failed = result.failed + 1 result.failed = result.failed + 1
# Bulk update bookmarks in DB # Bulk update bookmarks in DB
Bookmark.objects.bulk_update(bookmarks_to_update, ['url', Bookmark.objects.bulk_update(
'date_added', bookmarks_to_update,
'date_modified', [
'unread', "url",
'shared', "date_added",
'title', "date_modified",
'description', "unread",
'notes', "shared",
'owner']) "title",
"description",
"notes",
"owner",
],
)
# Bulk insert new bookmarks into DB # Bulk insert new bookmarks into DB
Bookmark.objects.bulk_create(bookmarks_to_create) Bookmark.objects.bulk_create(bookmarks_to_create)
@ -183,13 +198,20 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
for netscape_bookmark in netscape_bookmarks: for netscape_bookmark in netscape_bookmarks:
# Lookup bookmark by URL again # Lookup bookmark by URL again
bookmark = next( bookmark = next(
(bookmark for bookmark in existing_bookmarks if bookmark.url == netscape_bookmark.href), None) (
bookmark
for bookmark in existing_bookmarks
if bookmark.url == netscape_bookmark.href
),
None,
)
if not bookmark: if not bookmark:
# Something is wrong, we should have just created this bookmark # Something is wrong, we should have just created this bookmark
shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + '...' shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + "..."
logging.warning( logging.warning(
f'Failed to assign tags to the bookmark: {shortened_bookmark_tag_str}. Could not find bookmark by URL.') f"Failed to assign tags to the bookmark: {shortened_bookmark_tag_str}. Could not find bookmark by URL."
)
continue continue
# Get tag models by string, schedule inserts for bookmark -> tag associations # Get tag models by string, schedule inserts for bookmark -> tag associations
@ -201,7 +223,9 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark],
BookmarkToTagRelationShip.objects.bulk_create(relationships, ignore_conflicts=True) BookmarkToTagRelationShip.objects.bulk_create(relationships, ignore_conflicts=True)
def _copy_bookmark_data(netscape_bookmark: NetscapeBookmark, bookmark: Bookmark, options: ImportOptions): def _copy_bookmark_data(
netscape_bookmark: NetscapeBookmark, bookmark: Bookmark, options: ImportOptions
):
bookmark.url = netscape_bookmark.href bookmark.url = netscape_bookmark.href
if netscape_bookmark.date_added: if netscape_bookmark.date_added:
bookmark.date_added = parse_timestamp(netscape_bookmark.date_added) bookmark.date_added = parse_timestamp(netscape_bookmark.date_added)

View file

@ -25,29 +25,29 @@ class BookmarkParser(HTMLParser):
self.current_tag = None self.current_tag = None
self.bookmark = None self.bookmark = None
self.href = '' self.href = ""
self.add_date = '' self.add_date = ""
self.tags = '' self.tags = ""
self.title = '' self.title = ""
self.description = '' self.description = ""
self.notes = '' self.notes = ""
self.toread = '' self.toread = ""
self.private = '' self.private = ""
def handle_starttag(self, tag: str, attrs: list): def handle_starttag(self, tag: str, attrs: list):
name = 'handle_start_' + tag.lower() name = "handle_start_" + tag.lower()
if name in dir(self): if name in dir(self):
getattr(self, name)({k.lower(): v for k, v in attrs}) getattr(self, name)({k.lower(): v for k, v in attrs})
self.current_tag = tag self.current_tag = tag
def handle_endtag(self, tag: str): def handle_endtag(self, tag: str):
name = 'handle_end_' + tag.lower() name = "handle_end_" + tag.lower()
if name in dir(self): if name in dir(self):
getattr(self, name)() getattr(self, name)()
self.current_tag = None self.current_tag = None
def handle_data(self, data): def handle_data(self, data):
name = f'handle_{self.current_tag}_data' name = f"handle_{self.current_tag}_data"
if name in dir(self): if name in dir(self):
getattr(self, name)(data) getattr(self, name)(data)
@ -60,22 +60,22 @@ class BookmarkParser(HTMLParser):
def handle_start_a(self, attrs: Dict[str, str]): def handle_start_a(self, attrs: Dict[str, str]):
vars(self).update(attrs) vars(self).update(attrs)
tag_names = parse_tag_string(self.tags) tag_names = parse_tag_string(self.tags)
archived = 'linkding:archived' in self.tags archived = "linkding:archived" in self.tags
try: try:
tag_names.remove('linkding:archived') tag_names.remove("linkding:archived")
except ValueError: except ValueError:
pass pass
self.bookmark = NetscapeBookmark( self.bookmark = NetscapeBookmark(
href=self.href, href=self.href,
title='', title="",
description='', description="",
notes='', notes="",
date_added=self.add_date, date_added=self.add_date,
tag_names=tag_names, tag_names=tag_names,
to_read=self.toread == '1', to_read=self.toread == "1",
# Mark as private by default, also when attribute is not specified # Mark as private by default, also when attribute is not specified
private=self.private != '0', private=self.private != "0",
archived=archived, archived=archived,
) )
@ -84,9 +84,9 @@ class BookmarkParser(HTMLParser):
def handle_dd_data(self, data): def handle_dd_data(self, data):
desc = data.strip() desc = data.strip()
if '[linkding-notes]' in desc: if "[linkding-notes]" in desc:
self.notes = desc.split('[linkding-notes]')[1].split('[/linkding-notes]')[0] self.notes = desc.split("[linkding-notes]")[1].split("[/linkding-notes]")[0]
self.description = desc.split('[linkding-notes]')[0] self.description = desc.split("[linkding-notes]")[0]
def add_bookmark(self): def add_bookmark(self):
if self.bookmark: if self.bookmark:
@ -95,14 +95,14 @@ class BookmarkParser(HTMLParser):
self.bookmark.notes = self.notes self.bookmark.notes = self.notes
self.bookmarks.append(self.bookmark) self.bookmarks.append(self.bookmark)
self.bookmark = None self.bookmark = None
self.href = '' self.href = ""
self.add_date = '' self.add_date = ""
self.tags = '' self.tags = ""
self.title = '' self.title = ""
self.description = '' self.description = ""
self.notes = '' self.notes = ""
self.toread = '' self.toread = ""
self.private = '' self.private = ""
def parse(html: str) -> List[NetscapeBookmark]: def parse(html: str) -> List[NetscapeBookmark]:

View file

@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
def get_or_create_tags(tag_names: List[str], user: User): def get_or_create_tags(tag_names: List[str], user: User):
tags = [get_or_create_tag(tag_name, user) for tag_name in tag_names] tags = [get_or_create_tag(tag_name, user) for tag_name in tag_names]
return unique(tags, operator.attrgetter('id')) return unique(tags, operator.attrgetter("id"))
def get_or_create_tag(name: str, user: User): def get_or_create_tag(name: str, user: User):

View file

@ -18,8 +18,10 @@ logger = logging.getLogger(__name__)
def is_web_archive_integration_active(user: User) -> bool: def is_web_archive_integration_active(user: User) -> bool:
background_tasks_enabled = not settings.LD_DISABLE_BACKGROUND_TASKS background_tasks_enabled = not settings.LD_DISABLE_BACKGROUND_TASKS
web_archive_integration_enabled = \ web_archive_integration_enabled = (
user.profile.web_archive_integration == UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED user.profile.web_archive_integration
== UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED
)
return background_tasks_enabled and web_archive_integration_enabled return background_tasks_enabled and web_archive_integration_enabled
@ -31,28 +33,36 @@ def create_web_archive_snapshot(user: User, bookmark: Bookmark, force_update: bo
def _load_newest_snapshot(bookmark: Bookmark): def _load_newest_snapshot(bookmark: Bookmark):
try: try:
logger.info(f'Load existing snapshot for bookmark. url={bookmark.url}') logger.info(f"Load existing snapshot for bookmark. url={bookmark.url}")
cdx_api = bookmarks.services.wayback.CustomWaybackMachineCDXServerAPI(bookmark.url) cdx_api = bookmarks.services.wayback.CustomWaybackMachineCDXServerAPI(
bookmark.url
)
existing_snapshot = cdx_api.newest() existing_snapshot = cdx_api.newest()
if existing_snapshot: if existing_snapshot:
bookmark.web_archive_snapshot_url = existing_snapshot.archive_url bookmark.web_archive_snapshot_url = existing_snapshot.archive_url
bookmark.save(update_fields=['web_archive_snapshot_url']) bookmark.save(update_fields=["web_archive_snapshot_url"])
logger.info(f'Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}') logger.info(
f"Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}"
)
except NoCDXRecordFound: except NoCDXRecordFound:
logger.info(f'Could not find any snapshots for bookmark. url={bookmark.url}') logger.info(f"Could not find any snapshots for bookmark. url={bookmark.url}")
except WaybackError as error: except WaybackError as error:
logger.error(f'Failed to load existing snapshot. url={bookmark.url}', exc_info=error) logger.error(
f"Failed to load existing snapshot. url={bookmark.url}", exc_info=error
)
def _create_snapshot(bookmark: Bookmark): def _create_snapshot(bookmark: Bookmark):
logger.info(f'Create new snapshot for bookmark. url={bookmark.url}...') logger.info(f"Create new snapshot for bookmark. url={bookmark.url}...")
archive = waybackpy.WaybackMachineSaveAPI(bookmark.url, DEFAULT_USER_AGENT, max_tries=1) archive = waybackpy.WaybackMachineSaveAPI(
bookmark.url, DEFAULT_USER_AGENT, max_tries=1
)
archive.save() archive.save()
bookmark.web_archive_snapshot_url = archive.archive_url bookmark.web_archive_snapshot_url = archive.archive_url
bookmark.save(update_fields=['web_archive_snapshot_url']) bookmark.save(update_fields=["web_archive_snapshot_url"])
logger.info(f'Successfully created new snapshot for bookmark:. url={bookmark.url}') logger.info(f"Successfully created new snapshot for bookmark:. url={bookmark.url}")
@background() @background()
@ -72,10 +82,13 @@ def _create_web_archive_snapshot_task(bookmark_id: int, force_update: bool):
return return
except TooManyRequestsError: except TooManyRequestsError:
logger.error( logger.error(
f'Failed to create snapshot due to rate limiting, trying to load newest snapshot as fallback. url={bookmark.url}') f"Failed to create snapshot due to rate limiting, trying to load newest snapshot as fallback. url={bookmark.url}"
)
except WaybackError as error: except WaybackError as error:
logger.error(f'Failed to create snapshot, trying to load newest snapshot as fallback. url={bookmark.url}', logger.error(
exc_info=error) f"Failed to create snapshot, trying to load newest snapshot as fallback. url={bookmark.url}",
exc_info=error,
)
# Load the newest snapshot as fallback # Load the newest snapshot as fallback
_load_newest_snapshot(bookmark) _load_newest_snapshot(bookmark)
@ -102,7 +115,9 @@ def schedule_bookmarks_without_snapshots(user: User):
@background() @background()
def _schedule_bookmarks_without_snapshots_task(user_id: int): def _schedule_bookmarks_without_snapshots_task(user_id: int):
user = get_user_model().objects.get(id=user_id) user = get_user_model().objects.get(id=user_id)
bookmarks_without_snapshots = Bookmark.objects.filter(web_archive_snapshot_url__exact='', owner=user) bookmarks_without_snapshots = Bookmark.objects.filter(
web_archive_snapshot_url__exact="", owner=user
)
for bookmark in bookmarks_without_snapshots: for bookmark in bookmarks_without_snapshots:
# To prevent rate limit errors from the Wayback API only try to load the latest snapshots instead of creating # To prevent rate limit errors from the Wayback API only try to load the latest snapshots instead of creating
@ -128,14 +143,16 @@ def _load_favicon_task(bookmark_id: int):
except Bookmark.DoesNotExist: except Bookmark.DoesNotExist:
return return
logger.info(f'Load favicon for bookmark. url={bookmark.url}') logger.info(f"Load favicon for bookmark. url={bookmark.url}")
new_favicon_file = favicon_loader.load_favicon(bookmark.url) new_favicon_file = favicon_loader.load_favicon(bookmark.url)
if new_favicon_file != bookmark.favicon_file: if new_favicon_file != bookmark.favicon_file:
bookmark.favicon_file = new_favicon_file bookmark.favicon_file = new_favicon_file
bookmark.save(update_fields=['favicon_file']) bookmark.save(update_fields=["favicon_file"])
logger.info(f'Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon_file}') logger.info(
f"Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon_file}"
)
def schedule_bookmarks_without_favicons(user: User): def schedule_bookmarks_without_favicons(user: User):
@ -146,11 +163,13 @@ def schedule_bookmarks_without_favicons(user: User):
@background() @background()
def _schedule_bookmarks_without_favicons_task(user_id: int): def _schedule_bookmarks_without_favicons_task(user_id: int):
user = get_user_model().objects.get(id=user_id) user = get_user_model().objects.get(id=user_id)
bookmarks = Bookmark.objects.filter(favicon_file__exact='', owner=user) bookmarks = Bookmark.objects.filter(favicon_file__exact="", owner=user)
tasks = [] tasks = []
for bookmark in bookmarks: for bookmark in bookmarks:
task = Task.objects.new_task(task_name='bookmarks.services.tasks._load_favicon_task', args=(bookmark.id,)) task = Task.objects.new_task(
task_name="bookmarks.services.tasks._load_favicon_task", args=(bookmark.id,)
)
tasks.append(task) tasks.append(task)
Task.objects.bulk_create(tasks) Task.objects.bulk_create(tasks)
@ -168,7 +187,9 @@ def _schedule_refresh_favicons_task(user_id: int):
tasks = [] tasks = []
for bookmark in bookmarks: for bookmark in bookmarks:
task = Task.objects.new_task(task_name='bookmarks.services.tasks._load_favicon_task', args=(bookmark.id,)) task = Task.objects.new_task(
task_name="bookmarks.services.tasks._load_favicon_task", args=(bookmark.id,)
)
tasks.append(task) tasks.append(task)
Task.objects.bulk_create(tasks) Task.objects.bulk_create(tasks)

View file

@ -14,8 +14,10 @@ class CustomWaybackMachineCDXServerAPI(waybackpy.WaybackMachineCDXServerAPI):
def newest(self): def newest(self):
unix_timestamp = int(time.time()) unix_timestamp = int(time.time())
self.closest = waybackpy.utils.unix_timestamp_to_wayback_timestamp(unix_timestamp) self.closest = waybackpy.utils.unix_timestamp_to_wayback_timestamp(
self.sort = 'closest' unix_timestamp
)
self.sort = "closest"
self.limit = -5 self.limit = -5
newest_snapshot = None newest_snapshot = None
@ -37,4 +39,4 @@ class CustomWaybackMachineCDXServerAPI(waybackpy.WaybackMachineCDXServerAPI):
super().add_payload(payload) super().add_payload(payload)
# Set fastLatest query param, as we are only using this API to get the latest snapshot and using fastLatest # Set fastLatest query param, as we are only using this API to get the latest snapshot and using fastLatest
# makes searching for latest snapshots faster # makes searching for latest snapshots faster
payload['fastLatest'] = 'true' payload["fastLatest"] = "true"

View file

@ -18,9 +18,9 @@ class WebsiteMetadata:
def to_dict(self): def to_dict(self):
return { return {
'url': self.url, "url": self.url,
'title': self.title, "title": self.title,
'description': self.description, "description": self.description,
} }
@ -34,22 +34,29 @@ def load_website_metadata(url: str):
start = timezone.now() start = timezone.now()
page_text = load_page(url) page_text = load_page(url)
end = timezone.now() end = timezone.now()
logger.debug(f'Load duration: {end - start}') logger.debug(f"Load duration: {end - start}")
start = timezone.now() start = timezone.now()
soup = BeautifulSoup(page_text, 'html.parser') soup = BeautifulSoup(page_text, "html.parser")
title = soup.title.string.strip() if soup.title is not None else None title = soup.title.string.strip() if soup.title is not None else None
description_tag = soup.find('meta', attrs={'name': 'description'}) description_tag = soup.find("meta", attrs={"name": "description"})
description = description_tag['content'].strip() if description_tag and description_tag[ description = (
'content'] else None description_tag["content"].strip()
if description_tag and description_tag["content"]
else None
)
if not description: if not description:
description_tag = soup.find('meta', attrs={'property': 'og:description'}) description_tag = soup.find("meta", attrs={"property": "og:description"})
description = description_tag['content'].strip() if description_tag and description_tag['content'] else None description = (
description_tag["content"].strip()
if description_tag and description_tag["content"]
else None
)
end = timezone.now() end = timezone.now()
logger.debug(f'Parsing duration: {end - start}') logger.debug(f"Parsing duration: {end - start}")
finally: finally:
return WebsiteMetadata(url=url, title=title, description=description) return WebsiteMetadata(url=url, title=title, description=description)
@ -73,30 +80,30 @@ def load_page(url: str):
else: else:
content = content + chunk content = content + chunk
logger.debug(f'Loaded chunk (iteration={iteration}, total={size / 1024})') logger.debug(f"Loaded chunk (iteration={iteration}, total={size / 1024})")
# Stop reading if we have parsed end of head tag # Stop reading if we have parsed end of head tag
end_of_head = '</head>'.encode('utf-8') end_of_head = "</head>".encode("utf-8")
if end_of_head in content: if end_of_head in content:
logger.debug(f'Found closing head tag after {size} bytes') logger.debug(f"Found closing head tag after {size} bytes")
content = content.split(end_of_head)[0] + end_of_head content = content.split(end_of_head)[0] + end_of_head
break break
# Stop reading if we exceed limit # Stop reading if we exceed limit
if size > MAX_CONTENT_LIMIT: if size > MAX_CONTENT_LIMIT:
logger.debug(f'Cancel reading document after {size} bytes') logger.debug(f"Cancel reading document after {size} bytes")
break break
if hasattr(r, '_content_consumed'): if hasattr(r, "_content_consumed"):
logger.debug(f'Request consumed: {r._content_consumed}') logger.debug(f"Request consumed: {r._content_consumed}")
# Use charset_normalizer to determine encoding that best matches the response content # Use charset_normalizer to determine encoding that best matches the response content
# Several sites seem to specify the response encoding incorrectly, so we ignore it and use custom logic instead # Several sites seem to specify the response encoding incorrectly, so we ignore it and use custom logic instead
# This is different from Response.text which does respect the encoding specified in the response first, # This is different from Response.text which does respect the encoding specified in the response first,
# before trying to determine one # before trying to determine one
results = from_bytes(content or '') results = from_bytes(content or "")
return str(results.best()) return str(results.best())
DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36' DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36"
def fake_request_headers(): def fake_request_headers():

View file

@ -15,9 +15,11 @@ def user_logged_in(sender, request, user, **kwargs):
def extend_sqlite(connection=None, **kwargs): def extend_sqlite(connection=None, **kwargs):
# Load ICU extension into Sqlite connection to support case-insensitive # Load ICU extension into Sqlite connection to support case-insensitive
# comparisons with unicode characters # comparisons with unicode characters
if connection.vendor == 'sqlite' and settings.USE_SQLITE_ICU_EXTENSION: if connection.vendor == "sqlite" and settings.USE_SQLITE_ICU_EXTENSION:
connection.connection.enable_load_extension(True) connection.connection.enable_load_extension(True)
connection.connection.load_extension(settings.SQLITE_ICU_EXTENSION_PATH.rstrip('.so')) connection.connection.load_extension(
settings.SQLITE_ICU_EXTENSION_PATH.rstrip(".so")
)
with connection.cursor() as cursor: with connection.cursor() as cursor:
# Load an ICU collation for case-insensitive ordering. # Load an ICU collation for case-insensitive ordering.

View file

@ -2,48 +2,67 @@ from typing import List
from django import template from django import template
from bookmarks.models import BookmarkForm, BookmarkSearch, BookmarkSearchForm, Tag, build_tag_string, User from bookmarks.models import (
BookmarkForm,
BookmarkSearch,
BookmarkSearchForm,
Tag,
build_tag_string,
User,
)
register = template.Library() register = template.Library()
@register.inclusion_tag('bookmarks/form.html', name='bookmark_form', takes_context=True) @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): def bookmark_form(
context,
form: BookmarkForm,
cancel_url: str,
bookmark_id: int = 0,
auto_close: bool = False,
):
return { return {
'request': context['request'], "request": context["request"],
'form': form, "form": form,
'auto_close': auto_close, "auto_close": auto_close,
'bookmark_id': bookmark_id, "bookmark_id": bookmark_id,
'cancel_url': cancel_url "cancel_url": cancel_url,
} }
@register.inclusion_tag('bookmarks/search.html', name='bookmark_search', takes_context=True) @register.inclusion_tag(
def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ''): "bookmarks/search.html", name="bookmark_search", takes_context=True
)
def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ""):
tag_names = [tag.name for tag in tags] tag_names = [tag.name for tag in tags]
tags_string = build_tag_string(tag_names, ' ') tags_string = build_tag_string(tag_names, " ")
search_form = BookmarkSearchForm(search, editable_fields=['q']) search_form = BookmarkSearchForm(search, editable_fields=["q"])
if mode == 'shared': if mode == "shared":
preferences_form = BookmarkSearchForm(search, editable_fields=['sort']) preferences_form = BookmarkSearchForm(search, editable_fields=["sort"])
else: else:
preferences_form = BookmarkSearchForm(search, editable_fields=['sort', 'shared', 'unread']) preferences_form = BookmarkSearchForm(
search, editable_fields=["sort", "shared", "unread"]
)
return { return {
'request': context['request'], "request": context["request"],
'search': search, "search": search,
'search_form': search_form, "search_form": search_form,
'preferences_form': preferences_form, "preferences_form": preferences_form,
'tags_string': tags_string, "tags_string": tags_string,
'mode': mode, "mode": mode,
} }
@register.inclusion_tag('bookmarks/user_select.html', name='user_select', takes_context=True) @register.inclusion_tag(
"bookmarks/user_select.html", name="user_select", takes_context=True
)
def user_select(context, search: BookmarkSearch, users: List[User]): def user_select(context, search: BookmarkSearch, users: List[User]):
sorted_users = sorted(users, key=lambda x: str.lower(x.username)) sorted_users = sorted(users, key=lambda x: str.lower(x.username))
form = BookmarkSearchForm(search, editable_fields=['user'], users=sorted_users) form = BookmarkSearchForm(search, editable_fields=["user"], users=sorted_users)
return { return {
'search': search, "search": search,
'users': sorted_users, "users": sorted_users,
'form': form, "form": form,
} }

View file

@ -8,14 +8,15 @@ NUM_ADJACENT_PAGES = 2
register = template.Library() register = template.Library()
@register.inclusion_tag('bookmarks/pagination.html', name='pagination', takes_context=True) @register.inclusion_tag(
"bookmarks/pagination.html", name="pagination", takes_context=True
)
def pagination(context, page: Page): def pagination(context, page: Page):
visible_page_numbers = get_visible_page_numbers(page.number, page.paginator.num_pages) visible_page_numbers = get_visible_page_numbers(
page.number, page.paginator.num_pages
)
return { return {"page": page, "visible_page_numbers": visible_page_numbers}
'page': page,
'visible_page_numbers': visible_page_numbers
}
def get_visible_page_numbers(current_page_number: int, num_pages: int) -> [int]: def get_visible_page_numbers(current_page_number: int, num_pages: int) -> [int]:
@ -29,10 +30,12 @@ def get_visible_page_numbers(current_page_number: int, num_pages: int) -> [int]:
visible_pages = set() visible_pages = set()
# Add adjacent pages around current page # Add adjacent pages around current page
visible_pages |= set(range( visible_pages |= set(
range(
max(1, current_page_number - NUM_ADJACENT_PAGES), max(1, current_page_number - NUM_ADJACENT_PAGES),
min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1 min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1,
)) )
)
# Add first page # Add first page
visible_pages.add(1) visible_pages.add(1)

View file

@ -28,12 +28,12 @@ def add_tag_to_query(context, tag_name: str):
params = context.request.GET.copy() params = context.request.GET.copy()
# Append to or create query string # Append to or create query string
if params.__contains__('q'): if params.__contains__("q"):
query_string = params.__getitem__('q') + ' ' query_string = params.__getitem__("q") + " "
else: else:
query_string = '' query_string = ""
query_string = query_string + '#' + tag_name query_string = query_string + "#" + tag_name
params.__setitem__('q', query_string) params.__setitem__("q", query_string)
return params.urlencode() return params.urlencode()
@ -41,20 +41,26 @@ def add_tag_to_query(context, tag_name: str):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def remove_tag_from_query(context, tag_name: str): def remove_tag_from_query(context, tag_name: str):
params = context.request.GET.copy() params = context.request.GET.copy()
if params.__contains__('q'): if params.__contains__("q"):
# Split query string into parts # Split query string into parts
query_string = params.__getitem__('q') query_string = params.__getitem__("q")
query_parts = query_string.split() query_parts = query_string.split()
# Remove tag with hash # Remove tag with hash
tag_name_with_hash = '#' + tag_name tag_name_with_hash = "#" + tag_name
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name_with_hash)] query_parts = [
part
for part in query_parts
if str.lower(part) != str.lower(tag_name_with_hash)
]
# When using lax tag search, also remove tag without hash # When using lax tag search, also remove tag without hash
profile = context.request.user_profile profile = context.request.user_profile
if profile.tag_search == UserProfile.TAG_SEARCH_LAX: if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name)] query_parts = [
part for part in query_parts if str.lower(part) != str.lower(tag_name)
]
# Rebuild query string # Rebuild query string
query_string = ' '.join(query_parts) query_string = " ".join(query_parts)
params.__setitem__('q', query_string) params.__setitem__("q", query_string)
return params.urlencode() return params.urlencode()
@ -71,38 +77,38 @@ def replace_query_param(context, **kwargs):
return query.urlencode() return query.urlencode()
@register.filter(name='hash_tag') @register.filter(name="hash_tag")
def hash_tag(tag_name): def hash_tag(tag_name):
return '#' + tag_name return "#" + tag_name
@register.filter(name='first_char') @register.filter(name="first_char")
def first_char(text): def first_char(text):
return text[0] return text[0]
@register.filter(name='remaining_chars') @register.filter(name="remaining_chars")
def remaining_chars(text, index): def remaining_chars(text, index):
return text[index:] return text[index:]
@register.filter(name='humanize_absolute_date') @register.filter(name="humanize_absolute_date")
def humanize_absolute_date(value): def humanize_absolute_date(value):
if value in (None, ''): if value in (None, ""):
return '' return ""
return utils.humanize_absolute_date(value) return utils.humanize_absolute_date(value)
@register.filter(name='humanize_relative_date') @register.filter(name="humanize_relative_date")
def humanize_relative_date(value): def humanize_relative_date(value):
if value in (None, ''): if value in (None, ""):
return '' return ""
return utils.humanize_relative_date(value) return utils.humanize_relative_date(value)
@register.tag @register.tag
def htmlmin(parser, token): def htmlmin(parser, token):
nodelist = parser.parse(('endhtmlmin',)) nodelist = parser.parse(("endhtmlmin",))
parser.delete_first_token() parser.delete_first_token()
return HtmlMinNode(nodelist) return HtmlMinNode(nodelist)
@ -114,7 +120,7 @@ class HtmlMinNode(template.Node):
def render(self, context): def render(self, context):
output = self.nodelist.render(context) output = self.nodelist.render(context)
output = re.sub(r'\s+', ' ', output) output = re.sub(r"\s+", " ", output)
return output return output
@ -123,11 +129,11 @@ class HtmlMinNode(template.Node):
def render_markdown(context, markdown_text): def render_markdown(context, markdown_text):
# naive approach to reusing the renderer for a single request # naive approach to reusing the renderer for a single request
# works for bookmark list for now # works for bookmark list for now
if not ('markdown_renderer' in context): if not ("markdown_renderer" in context):
renderer = markdown.Markdown(extensions=['fenced_code', 'nl2br']) renderer = markdown.Markdown(extensions=["fenced_code", "nl2br"])
context['markdown_renderer'] = renderer context["markdown_renderer"] = renderer
else: else:
renderer = context['markdown_renderer'] renderer = context["markdown_renderer"]
as_html = renderer.convert(markdown_text) as_html = renderer.convert(markdown_text)
sanitized_html = bleach.clean(as_html, markdown_tags, markdown_attrs) sanitized_html = bleach.clean(as_html, markdown_tags, markdown_attrs)

View file

@ -18,24 +18,27 @@ class BookmarkFactoryMixin:
def get_or_create_test_user(self): def get_or_create_test_user(self):
if self.user is None: if self.user is None:
self.user = User.objects.create_user('testuser', 'test@example.com', 'password123') self.user = User.objects.create_user(
"testuser", "test@example.com", "password123"
)
return self.user return self.user
def setup_bookmark(self, def setup_bookmark(
self,
is_archived: bool = False, is_archived: bool = False,
unread: bool = False, unread: bool = False,
shared: bool = False, shared: bool = False,
tags=None, tags=None,
user: User = None, user: User = None,
url: str = '', url: str = "",
title: str = None, title: str = None,
description: str = '', description: str = "",
notes: str = '', notes: str = "",
website_title: str = '', website_title: str = "",
website_description: str = '', website_description: str = "",
web_archive_snapshot_url: str = '', web_archive_snapshot_url: str = "",
favicon_file: str = '', favicon_file: str = "",
added: datetime = None, added: datetime = None,
): ):
if title is None: if title is None:
@ -46,7 +49,7 @@ class BookmarkFactoryMixin:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
if not url: if not url:
unique_id = get_random_string(length=32) unique_id = get_random_string(length=32)
url = 'https://example.com/' + unique_id url = "https://example.com/" + unique_id
if added is None: if added is None:
added = timezone.now() added = timezone.now()
bookmark = Bookmark( bookmark = Bookmark(
@ -71,49 +74,53 @@ class BookmarkFactoryMixin:
bookmark.save() bookmark.save()
return bookmark return bookmark
def setup_numbered_bookmarks(self, def setup_numbered_bookmarks(
self,
count: int, count: int,
prefix: str = '', prefix: str = "",
suffix: str = '', suffix: str = "",
tag_prefix: str = '', tag_prefix: str = "",
archived: bool = False, archived: bool = False,
unread: bool = False, unread: bool = False,
shared: bool = False, shared: bool = False,
with_tags: bool = False, with_tags: bool = False,
user: User = None): user: User = None,
):
user = user or self.get_or_create_test_user() user = user or self.get_or_create_test_user()
bookmarks = [] bookmarks = []
if not prefix: if not prefix:
if archived: if archived:
prefix = 'Archived Bookmark' prefix = "Archived Bookmark"
elif shared: elif shared:
prefix = 'Shared Bookmark' prefix = "Shared Bookmark"
else: else:
prefix = 'Bookmark' prefix = "Bookmark"
if not tag_prefix: if not tag_prefix:
if archived: if archived:
tag_prefix = 'Archived Tag' tag_prefix = "Archived Tag"
elif shared: elif shared:
tag_prefix = 'Shared Tag' tag_prefix = "Shared Tag"
else: else:
tag_prefix = 'Tag' tag_prefix = "Tag"
for i in range(1, count + 1): for i in range(1, count + 1):
title = f'{prefix} {i}{suffix}' title = f"{prefix} {i}{suffix}"
url = f'https://example.com/{prefix}/{i}' url = f"https://example.com/{prefix}/{i}"
tags = [] tags = []
if with_tags: if with_tags:
tag_name = f'{tag_prefix} {i}{suffix}' tag_name = f"{tag_prefix} {i}{suffix}"
tags = [self.setup_tag(name=tag_name, user=user)] tags = [self.setup_tag(name=tag_name, user=user)]
bookmark = self.setup_bookmark(url=url, bookmark = self.setup_bookmark(
url=url,
title=title, title=title,
is_archived=archived, is_archived=archived,
unread=unread, unread=unread,
shared=shared, shared=shared,
tags=tags, tags=tags,
user=user) user=user,
)
bookmarks.append(bookmark) bookmarks.append(bookmark)
return bookmarks return bookmarks
@ -121,7 +128,7 @@ class BookmarkFactoryMixin:
def get_numbered_bookmark(self, title: str): def get_numbered_bookmark(self, title: str):
return Bookmark.objects.get(title=title) return Bookmark.objects.get(title=title)
def setup_tag(self, user: User = None, name: str = ''): def setup_tag(self, user: User = None, name: str = ""):
if user is None: if user is None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
if not name: if not name:
@ -130,10 +137,15 @@ class BookmarkFactoryMixin:
tag.save() tag.save()
return tag return tag
def setup_user(self, name: str = None, enable_sharing: bool = False, enable_public_sharing: bool = False): def setup_user(
self,
name: str = None,
enable_sharing: bool = False,
enable_public_sharing: bool = False,
):
if not name: if not name:
name = get_random_string(length=32) name = get_random_string(length=32)
user = User.objects.create_user(name, 'user@example.com', 'password123') user = User.objects.create_user(name, "user@example.com", "password123")
user.profile.enable_sharing = enable_sharing user.profile.enable_sharing = enable_sharing
user.profile.enable_public_sharing = enable_public_sharing user.profile.enable_public_sharing = enable_public_sharing
user.profile.save() user.profile.save()
@ -161,17 +173,17 @@ class LinkdingApiTestCase(APITestCase):
return response return response
def post(self, url, data=None, expected_status_code=status.HTTP_200_OK): def post(self, url, data=None, expected_status_code=status.HTTP_200_OK):
response = self.client.post(url, data, format='json') response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, expected_status_code) self.assertEqual(response.status_code, expected_status_code)
return response return response
def put(self, url, data=None, expected_status_code=status.HTTP_200_OK): def put(self, url, data=None, expected_status_code=status.HTTP_200_OK):
response = self.client.put(url, data, format='json') response = self.client.put(url, data, format="json")
self.assertEqual(response.status_code, expected_status_code) self.assertEqual(response.status_code, expected_status_code)
return response return response
def patch(self, url, data=None, expected_status_code=status.HTTP_200_OK): def patch(self, url, data=None, expected_status_code=status.HTTP_200_OK):
response = self.client.patch(url, data, format='json') response = self.client.patch(url, data, format="json")
self.assertEqual(response.status_code, expected_status_code) self.assertEqual(response.status_code, expected_status_code)
return response return response
@ -182,14 +194,16 @@ class LinkdingApiTestCase(APITestCase):
class BookmarkHtmlTag: class BookmarkHtmlTag:
def __init__(self, def __init__(
href: str = '', self,
title: str = '', href: str = "",
description: str = '', title: str = "",
add_date: str = '', description: str = "",
tags: str = '', add_date: str = "",
tags: str = "",
to_read: bool = False, to_read: bool = False,
private: bool = True): private: bool = True,
):
self.href = href self.href = href
self.title = title self.title = title
self.description = description self.description = description
@ -201,7 +215,7 @@ class BookmarkHtmlTag:
class ImportTestMixin: class ImportTestMixin:
def render_tag(self, tag: BookmarkHtmlTag): def render_tag(self, tag: BookmarkHtmlTag):
return f''' return f"""
<DT> <DT>
<A {f'HREF="{tag.href}"' if tag.href else ''} <A {f'HREF="{tag.href}"' if tag.href else ''}
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''} {f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''}
@ -211,13 +225,13 @@ class ImportTestMixin:
{tag.title if tag.title else ''} {tag.title if tag.title else ''}
</A> </A>
{f'<DD>{tag.description}' if tag.description else ''} {f'<DD>{tag.description}' if tag.description else ''}
''' """
def render_html(self, tags: List[BookmarkHtmlTag] = None, tags_html: str = ''): def render_html(self, tags: List[BookmarkHtmlTag] = None, tags_html: str = ""):
if tags: if tags:
rendered_tags = [self.render_tag(tag) for tag in tags] rendered_tags = [self.render_tag(tag) for tag in tags]
tags_html = '\n'.join(rendered_tags) tags_html = "\n".join(rendered_tags)
return f''' return f"""
<!DOCTYPE NETSCAPE-Bookmark-file-1> <!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE> <TITLE>Bookmarks</TITLE>
@ -225,34 +239,34 @@ class ImportTestMixin:
<DL><p> <DL><p>
{tags_html} {tags_html}
</DL><p> </DL><p>
''' """
_words = [ _words = [
'quasi', "quasi",
'consequatur', "consequatur",
'necessitatibus', "necessitatibus",
'debitis', "debitis",
'quod', "quod",
'vero', "vero",
'qui', "qui",
'commodi', "commodi",
'quod', "quod",
'odio', "odio",
'aliquam', "aliquam",
'veniam', "veniam",
'architecto', "architecto",
'consequatur', "consequatur",
'autem', "autem",
'qui', "qui",
'iste', "iste",
'asperiores', "asperiores",
'soluta', "soluta",
'et', "et",
] ]
def random_sentence(num_words: int = None, including_word: str = ''): def random_sentence(num_words: int = None, including_word: str = ""):
if num_words is None: if num_words is None:
num_words = random.randint(5, 10) num_words = random.randint(5, 10)
selected_words = random.choices(_words, k=num_words) selected_words = random.choices(_words, k=num_words)
@ -260,7 +274,7 @@ def random_sentence(num_words: int = None, including_word: str = ''):
selected_words.append(including_word) selected_words.append(including_word)
random.shuffle(selected_words) random.shuffle(selected_words)
return ' '.join(selected_words) return " ".join(selected_words)
def disable_logging(f): def disable_logging(f):
@ -275,5 +289,5 @@ def disable_logging(f):
def collapse_whitespace(text: str): def collapse_whitespace(text: str):
text = text.replace('\n', '').replace('\r', '') text = text.replace("\n", "").replace("\r", "")
return ' '.join(text.split()) return " ".join(text.split())

View file

@ -6,21 +6,24 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class AnonymousViewTestCase(TestCase, BookmarkFactoryMixin): class AnonymousViewTestCase(TestCase, BookmarkFactoryMixin):
def assertSharedBookmarksLinkCount(self, response, count): def assertSharedBookmarksLinkCount(self, response, count):
url = reverse('bookmarks:shared') url = reverse("bookmarks:shared")
self.assertContains(response, f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>', self.assertContains(
count=count) response,
f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
count=count,
)
def test_publicly_shared_bookmarks_link(self): def test_publicly_shared_bookmarks_link(self):
# should not render link if no public shares exist # should not render link if no public shares exist
user = self.setup_user(enable_sharing=True) user = self.setup_user(enable_sharing=True)
self.setup_bookmark(user=user, shared=True) self.setup_bookmark(user=user, shared=True)
response = self.client.get(reverse('login')) response = self.client.get(reverse("login"))
self.assertSharedBookmarksLinkCount(response, 0) self.assertSharedBookmarksLinkCount(response, 0)
# should render link if public shares exist # should render link if public shares exist
user.profile.enable_public_sharing = True user.profile.enable_public_sharing = True
user.profile.save() user.profile.save()
response = self.client.get(reverse('login')) response = self.client.get(reverse("login"))
self.assertSharedBookmarksLinkCount(response, 1) self.assertSharedBookmarksLinkCount(response, 1)

View file

@ -7,23 +7,35 @@ from django.test import TestCase
class AppOptionsTestCase(TestCase): class AppOptionsTestCase(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.settings_module = importlib.import_module('siteroot.settings.base') self.settings_module = importlib.import_module("siteroot.settings.base")
def test_empty_csrf_trusted_origins(self): def test_empty_csrf_trusted_origins(self):
module = importlib.reload(self.settings_module) module = importlib.reload(self.settings_module)
self.assertFalse(hasattr(module, 'CSRF_TRUSTED_ORIGINS')) self.assertFalse(hasattr(module, "CSRF_TRUSTED_ORIGINS"))
@mock.patch.dict(os.environ, {'LD_CSRF_TRUSTED_ORIGINS': 'https://linkding.example.com'}) @mock.patch.dict(
os.environ, {"LD_CSRF_TRUSTED_ORIGINS": "https://linkding.example.com"}
)
def test_single_csrf_trusted_origin(self): def test_single_csrf_trusted_origin(self):
module = importlib.reload(self.settings_module) module = importlib.reload(self.settings_module)
self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS')) self.assertTrue(hasattr(module, "CSRF_TRUSTED_ORIGINS"))
self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com']) self.assertCountEqual(
module.CSRF_TRUSTED_ORIGINS, ["https://linkding.example.com"]
)
@mock.patch.dict(os.environ, {'LD_CSRF_TRUSTED_ORIGINS': 'https://linkding.example.com,http://linkding.example.com'}) @mock.patch.dict(
os.environ,
{
"LD_CSRF_TRUSTED_ORIGINS": "https://linkding.example.com,http://linkding.example.com"
},
)
def test_multiple_csrf_trusted_origin(self): def test_multiple_csrf_trusted_origin(self):
module = importlib.reload(self.settings_module) module = importlib.reload(self.settings_module)
self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS')) self.assertTrue(hasattr(module, "CSRF_TRUSTED_ORIGINS"))
self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com', 'http://linkding.example.com']) self.assertCountEqual(
module.CSRF_TRUSTED_ORIGINS,
["https://linkding.example.com", "http://linkding.example.com"],
)

View file

@ -10,37 +10,49 @@ class AuthProxySupportTest(TestCase):
# Reproducing configuration from the settings logic here # Reproducing configuration from the settings logic here
# ideally this test would just override the respective options # ideally this test would just override the respective options
@modify_settings( @modify_settings(
MIDDLEWARE={'append': 'bookmarks.middlewares.CustomRemoteUserMiddleware'}, MIDDLEWARE={"append": "bookmarks.middlewares.CustomRemoteUserMiddleware"},
AUTHENTICATION_BACKENDS={'prepend': 'django.contrib.auth.backends.RemoteUserBackend'} AUTHENTICATION_BACKENDS={
"prepend": "django.contrib.auth.backends.RemoteUserBackend"
},
) )
def test_auth_proxy_authentication(self): def test_auth_proxy_authentication(self):
user = User.objects.create_user('auth_proxy_user', 'user@example.com', 'password123') user = User.objects.create_user(
"auth_proxy_user", "user@example.com", "password123"
)
headers = {'REMOTE_USER': user.username} headers = {"REMOTE_USER": user.username}
response = self.client.get(reverse('bookmarks:index'), **headers) response = self.client.get(reverse("bookmarks:index"), **headers)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Reproducing configuration from the settings logic here # Reproducing configuration from the settings logic here
# ideally this test would just override the respective options # ideally this test would just override the respective options
@modify_settings( @modify_settings(
MIDDLEWARE={'append': 'bookmarks.middlewares.CustomRemoteUserMiddleware'}, MIDDLEWARE={"append": "bookmarks.middlewares.CustomRemoteUserMiddleware"},
AUTHENTICATION_BACKENDS={'prepend': 'django.contrib.auth.backends.RemoteUserBackend'} AUTHENTICATION_BACKENDS={
"prepend": "django.contrib.auth.backends.RemoteUserBackend"
},
) )
def test_auth_proxy_with_custom_header(self): def test_auth_proxy_with_custom_header(self):
with patch.object(CustomRemoteUserMiddleware, 'header', new_callable=PropertyMock) as mock: with patch.object(
mock.return_value = 'Custom-User' CustomRemoteUserMiddleware, "header", new_callable=PropertyMock
user = User.objects.create_user('auth_proxy_user', 'user@example.com', 'password123') ) as mock:
mock.return_value = "Custom-User"
user = User.objects.create_user(
"auth_proxy_user", "user@example.com", "password123"
)
headers = {'Custom-User': user.username} headers = {"Custom-User": user.username}
response = self.client.get(reverse('bookmarks:index'), **headers) response = self.client.get(reverse("bookmarks:index"), **headers)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_auth_proxy_is_disabled_by_default(self): def test_auth_proxy_is_disabled_by_default(self):
user = User.objects.create_user('auth_proxy_user', 'user@example.com', 'password123') user = User.objects.create_user(
"auth_proxy_user", "user@example.com", "password123"
)
headers = {'REMOTE_USER': user.username} headers = {"REMOTE_USER": user.username}
response = self.client.get(reverse('bookmarks:index'), **headers, follow=True) response = self.client.get(reverse("bookmarks:index"), **headers, follow=True)
self.assertRedirects(response, '/login/?next=%2Fbookmarks') self.assertRedirects(response, "/login/?next=%2Fbookmarks")

View file

@ -17,26 +17,37 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(len(bookmarks), Bookmark.objects.count()) self.assertEqual(len(bookmarks), Bookmark.objects.count())
for bookmark in bookmarks: for bookmark in bookmarks:
self.assertEqual(model_to_dict(bookmark), model_to_dict(Bookmark.objects.get(id=bookmark.id))) self.assertEqual(
model_to_dict(bookmark),
model_to_dict(Bookmark.objects.get(id=bookmark.id)),
)
def test_archive_should_archive_bookmark(self): def test_archive_should_archive_bookmark(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'archive': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"archive": [bookmark.id],
},
)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertTrue(bookmark.is_archived) self.assertTrue(bookmark.is_archived)
def test_can_only_archive_own_bookmarks(self): def test_can_only_archive_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark = self.setup_bookmark(user=other_user) bookmark = self.setup_bookmark(user=other_user)
response = self.client.post(reverse('bookmarks:index.action'), { response = self.client.post(
'archive': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"archive": [bookmark.id],
},
)
bookmark.refresh_from_db() bookmark.refresh_from_db()
@ -46,20 +57,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
def test_unarchive_should_unarchive_bookmark(self): def test_unarchive_should_unarchive_bookmark(self):
bookmark = self.setup_bookmark(is_archived=True) bookmark = self.setup_bookmark(is_archived=True)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'unarchive': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"unarchive": [bookmark.id],
},
)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertFalse(bookmark.is_archived) self.assertFalse(bookmark.is_archived)
def test_unarchive_can_only_archive_own_bookmarks(self): def test_unarchive_can_only_archive_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark = self.setup_bookmark(is_archived=True, user=other_user) bookmark = self.setup_bookmark(is_archived=True, user=other_user)
response = self.client.post(reverse('bookmarks:index.action'), { response = self.client.post(
'unarchive': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"unarchive": [bookmark.id],
},
)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
@ -68,28 +87,39 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
def test_delete_should_delete_bookmark(self): def test_delete_should_delete_bookmark(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'remove': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"remove": [bookmark.id],
},
)
self.assertEqual(Bookmark.objects.count(), 0) self.assertEqual(Bookmark.objects.count(), 0)
def test_delete_can_only_delete_own_bookmarks(self): def test_delete_can_only_delete_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark = self.setup_bookmark(user=other_user) bookmark = self.setup_bookmark(user=other_user)
response = self.client.post(reverse('bookmarks:index.action'), { response = self.client.post(
'remove': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"remove": [bookmark.id],
},
)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertTrue(Bookmark.objects.filter(id=bookmark.id).exists()) self.assertTrue(Bookmark.objects.filter(id=bookmark.id).exists())
def test_mark_as_read(self): def test_mark_as_read(self):
bookmark = self.setup_bookmark(unread=True) bookmark = self.setup_bookmark(unread=True)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'mark_as_read': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"mark_as_read": [bookmark.id],
},
)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertFalse(bookmark.unread) self.assertFalse(bookmark.unread)
@ -97,21 +127,29 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
def test_unshare_should_unshare_bookmark(self): def test_unshare_should_unshare_bookmark(self):
bookmark = self.setup_bookmark(shared=True) bookmark = self.setup_bookmark(shared=True)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'unshare': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"unshare": [bookmark.id],
},
)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertFalse(bookmark.shared) self.assertFalse(bookmark.shared)
def test_can_only_unshare_own_bookmarks(self): def test_can_only_unshare_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark = self.setup_bookmark(user=other_user, shared=True) bookmark = self.setup_bookmark(user=other_user, shared=True)
response = self.client.post(reverse('bookmarks:index.action'), { response = self.client.post(
'unshare': [bookmark.id], reverse("bookmarks:index.action"),
}) {
"unshare": [bookmark.id],
},
)
bookmark.refresh_from_db() bookmark.refresh_from_db()
@ -123,27 +161,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_archive'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_archive"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
def test_can_only_bulk_archive_own_bookmarks(self): def test_can_only_bulk_archive_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(user=other_user) bookmark1 = self.setup_bookmark(user=other_user)
bookmark2 = self.setup_bookmark(user=other_user) bookmark2 = self.setup_bookmark(user=other_user)
bookmark3 = self.setup_bookmark(user=other_user) bookmark3 = self.setup_bookmark(user=other_user)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_archive'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_archive"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -154,27 +208,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(is_archived=True) bookmark2 = self.setup_bookmark(is_archived=True)
bookmark3 = self.setup_bookmark(is_archived=True) bookmark3 = self.setup_bookmark(is_archived=True)
self.client.post(reverse('bookmarks:archived.action'), { self.client.post(
'bulk_action': ['bulk_unarchive'], reverse("bookmarks:archived.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_unarchive"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
def test_can_only_bulk_unarchive_own_bookmarks(self): def test_can_only_bulk_unarchive_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(is_archived=True, user=other_user) bookmark1 = self.setup_bookmark(is_archived=True, user=other_user)
bookmark2 = self.setup_bookmark(is_archived=True, user=other_user) bookmark2 = self.setup_bookmark(is_archived=True, user=other_user)
bookmark3 = self.setup_bookmark(is_archived=True, user=other_user) bookmark3 = self.setup_bookmark(is_archived=True, user=other_user)
self.client.post(reverse('bookmarks:archived.action'), { self.client.post(
'bulk_action': ['bulk_unarchive'], reverse("bookmarks:archived.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_unarchive"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -185,27 +255,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_delete'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_delete"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
def test_can_only_bulk_delete_own_bookmarks(self): def test_can_only_bulk_delete_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(user=other_user) bookmark1 = self.setup_bookmark(user=other_user)
bookmark2 = self.setup_bookmark(user=other_user) bookmark2 = self.setup_bookmark(user=other_user)
bookmark3 = self.setup_bookmark(user=other_user) bookmark3 = self.setup_bookmark(user=other_user)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_delete'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_delete"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark1.id).first()) self.assertIsNotNone(Bookmark.objects.filter(id=bookmark1.id).first())
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark2.id).first()) self.assertIsNotNone(Bookmark.objects.filter(id=bookmark2.id).first())
@ -218,12 +304,19 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_tag'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bulk_tag_string': [f'{tag1.name} {tag2.name}'], "bulk_action": ["bulk_tag"],
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_execute": [""],
}) "bulk_tag_string": [f"{tag1.name} {tag2.name}"],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -234,19 +327,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2]) self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
def test_can_only_bulk_tag_own_bookmarks(self): def test_can_only_bulk_tag_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(user=other_user) bookmark1 = self.setup_bookmark(user=other_user)
bookmark2 = self.setup_bookmark(user=other_user) bookmark2 = self.setup_bookmark(user=other_user)
bookmark3 = self.setup_bookmark(user=other_user) bookmark3 = self.setup_bookmark(user=other_user)
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_tag'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bulk_tag_string': [f'{tag1.name} {tag2.name}'], "bulk_action": ["bulk_tag"],
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_execute": [""],
}) "bulk_tag_string": [f"{tag1.name} {tag2.name}"],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -263,12 +365,19 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(tags=[tag1, tag2]) bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
bookmark3 = self.setup_bookmark(tags=[tag1, tag2]) bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_untag'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bulk_tag_string': [f'{tag1.name} {tag2.name}'], "bulk_action": ["bulk_untag"],
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_execute": [""],
}) "bulk_tag_string": [f"{tag1.name} {tag2.name}"],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -279,19 +388,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertCountEqual(bookmark3.tags.all(), []) self.assertCountEqual(bookmark3.tags.all(), [])
def test_can_only_bulk_untag_own_bookmarks(self): def test_can_only_bulk_untag_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
bookmark1 = self.setup_bookmark(tags=[tag1, tag2], user=other_user) bookmark1 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
bookmark2 = self.setup_bookmark(tags=[tag1, tag2], user=other_user) bookmark2 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
bookmark3 = self.setup_bookmark(tags=[tag1, tag2], user=other_user) bookmark3 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_untag'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bulk_tag_string': [f'{tag1.name} {tag2.name}'], "bulk_action": ["bulk_untag"],
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_execute": [""],
}) "bulk_tag_string": [f"{tag1.name} {tag2.name}"],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -306,27 +424,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(unread=True) bookmark2 = self.setup_bookmark(unread=True)
bookmark3 = self.setup_bookmark(unread=True) bookmark3 = self.setup_bookmark(unread=True)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_read'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_read"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
def test_can_only_bulk_mark_as_read_own_bookmarks(self): def test_can_only_bulk_mark_as_read_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(unread=True, user=other_user) bookmark1 = self.setup_bookmark(unread=True, user=other_user)
bookmark2 = self.setup_bookmark(unread=True, user=other_user) bookmark2 = self.setup_bookmark(unread=True, user=other_user)
bookmark3 = self.setup_bookmark(unread=True, user=other_user) bookmark3 = self.setup_bookmark(unread=True, user=other_user)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_read'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_read"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
@ -337,27 +471,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(unread=False) bookmark2 = self.setup_bookmark(unread=False)
bookmark3 = self.setup_bookmark(unread=False) bookmark3 = self.setup_bookmark(unread=False)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_unread'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_unread"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
def test_can_only_bulk_mark_as_unread_own_bookmarks(self): def test_can_only_bulk_mark_as_unread_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(unread=False, user=other_user) bookmark1 = self.setup_bookmark(unread=False, user=other_user)
bookmark2 = self.setup_bookmark(unread=False, user=other_user) bookmark2 = self.setup_bookmark(unread=False, user=other_user)
bookmark3 = self.setup_bookmark(unread=False, user=other_user) bookmark3 = self.setup_bookmark(unread=False, user=other_user)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_unread'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_unread"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
@ -368,27 +518,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(shared=False) bookmark2 = self.setup_bookmark(shared=False)
bookmark3 = self.setup_bookmark(shared=False) bookmark3 = self.setup_bookmark(shared=False)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_share'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_share"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared)
def test_can_only_bulk_share_own_bookmarks(self): def test_can_only_bulk_share_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(shared=False, user=other_user) bookmark1 = self.setup_bookmark(shared=False, user=other_user)
bookmark2 = self.setup_bookmark(shared=False, user=other_user) bookmark2 = self.setup_bookmark(shared=False, user=other_user)
bookmark3 = self.setup_bookmark(shared=False, user=other_user) bookmark3 = self.setup_bookmark(shared=False, user=other_user)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_share'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_share"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
@ -399,27 +565,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(shared=True) bookmark2 = self.setup_bookmark(shared=True)
bookmark3 = self.setup_bookmark(shared=True) bookmark3 = self.setup_bookmark(shared=True)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_unshare'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_unshare"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared)
def test_can_only_bulk_unshare_own_bookmarks(self): def test_can_only_bulk_unshare_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(shared=True, user=other_user) bookmark1 = self.setup_bookmark(shared=True, user=other_user)
bookmark2 = self.setup_bookmark(shared=True, user=other_user) bookmark2 = self.setup_bookmark(shared=True, user=other_user)
bookmark3 = self.setup_bookmark(shared=True, user=other_user) bookmark3 = self.setup_bookmark(shared=True, user=other_user)
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_unshare'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_unshare"],
}) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
@ -430,11 +612,14 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_archive'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bulk_select_across': ['on'], "bulk_action": ["bulk_archive"],
}) "bulk_execute": [""],
"bulk_select_across": ["on"],
},
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -443,11 +628,14 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
def test_bulk_select_across_ignores_page(self): def test_bulk_select_across_ignores_page(self):
self.setup_numbered_bookmarks(100) self.setup_numbered_bookmarks(100)
self.client.post(reverse('bookmarks:index.action') + '?page=2', { self.client.post(
'bulk_action': ['bulk_delete'], reverse("bookmarks:index.action") + "?page=2",
'bulk_execute': [''], {
'bulk_select_across': ['on'], "bulk_action": ["bulk_delete"],
}) "bulk_execute": [""],
"bulk_select_across": ["on"],
},
)
self.assertEqual(0, Bookmark.objects.count()) self.assertEqual(0, Bookmark.objects.count())
@ -455,85 +643,108 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
# create a number of bookmarks with different states / visibility # create a number of bookmarks with different states / visibility
self.setup_numbered_bookmarks(3, with_tags=True) self.setup_numbered_bookmarks(3, with_tags=True)
self.setup_numbered_bookmarks(3, with_tags=True, archived=True) self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
self.setup_numbered_bookmarks(3, self.setup_numbered_bookmarks(
3,
shared=True, shared=True,
prefix="Joe's Bookmark", prefix="Joe's Bookmark",
user=self.setup_user(enable_sharing=True)) user=self.setup_user(enable_sharing=True),
)
def test_index_action_bulk_select_across_only_affects_active_bookmarks(self): def test_index_action_bulk_select_across_only_affects_active_bookmarks(self):
self.setup_bulk_edit_scope_test_data() self.setup_bulk_edit_scope_test_data()
self.assertIsNotNone(Bookmark.objects.filter(title='Bookmark 1').first()) self.assertIsNotNone(Bookmark.objects.filter(title="Bookmark 1").first())
self.assertIsNotNone(Bookmark.objects.filter(title='Bookmark 2').first()) self.assertIsNotNone(Bookmark.objects.filter(title="Bookmark 2").first())
self.assertIsNotNone(Bookmark.objects.filter(title='Bookmark 3').first()) self.assertIsNotNone(Bookmark.objects.filter(title="Bookmark 3").first())
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bulk_action': ['bulk_delete'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bulk_select_across': ['on'], "bulk_action": ["bulk_delete"],
}) "bulk_execute": [""],
"bulk_select_across": ["on"],
},
)
self.assertEqual(6, Bookmark.objects.count()) self.assertEqual(6, Bookmark.objects.count())
self.assertIsNone(Bookmark.objects.filter(title='Bookmark 1').first()) self.assertIsNone(Bookmark.objects.filter(title="Bookmark 1").first())
self.assertIsNone(Bookmark.objects.filter(title='Bookmark 2').first()) self.assertIsNone(Bookmark.objects.filter(title="Bookmark 2").first())
self.assertIsNone(Bookmark.objects.filter(title='Bookmark 3').first()) self.assertIsNone(Bookmark.objects.filter(title="Bookmark 3").first())
def test_index_action_bulk_select_across_respects_query(self): def test_index_action_bulk_select_across_respects_query(self):
self.setup_numbered_bookmarks(3, prefix='foo') self.setup_numbered_bookmarks(3, prefix="foo")
self.setup_numbered_bookmarks(3, prefix='bar') self.setup_numbered_bookmarks(3, prefix="bar")
self.assertEqual(3, Bookmark.objects.filter(title__startswith='foo').count()) self.assertEqual(3, Bookmark.objects.filter(title__startswith="foo").count())
self.client.post(reverse('bookmarks:index.action') + '?q=foo', { self.client.post(
'bulk_action': ['bulk_delete'], reverse("bookmarks:index.action") + "?q=foo",
'bulk_execute': [''], {
'bulk_select_across': ['on'], "bulk_action": ["bulk_delete"],
}) "bulk_execute": [""],
"bulk_select_across": ["on"],
},
)
self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count()) self.assertEqual(0, Bookmark.objects.filter(title__startswith="foo").count())
self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count()) self.assertEqual(3, Bookmark.objects.filter(title__startswith="bar").count())
def test_archived_action_bulk_select_across_only_affects_archived_bookmarks(self): def test_archived_action_bulk_select_across_only_affects_archived_bookmarks(self):
self.setup_bulk_edit_scope_test_data() self.setup_bulk_edit_scope_test_data()
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 1').first()) self.assertIsNotNone(
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 2').first()) Bookmark.objects.filter(title="Archived Bookmark 1").first()
self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 3').first()) )
self.assertIsNotNone(
Bookmark.objects.filter(title="Archived Bookmark 2").first()
)
self.assertIsNotNone(
Bookmark.objects.filter(title="Archived Bookmark 3").first()
)
self.client.post(reverse('bookmarks:archived.action'), { self.client.post(
'bulk_action': ['bulk_delete'], reverse("bookmarks:archived.action"),
'bulk_execute': [''], {
'bulk_select_across': ['on'], "bulk_action": ["bulk_delete"],
}) "bulk_execute": [""],
"bulk_select_across": ["on"],
},
)
self.assertEqual(6, Bookmark.objects.count()) self.assertEqual(6, Bookmark.objects.count())
self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 1').first()) self.assertIsNone(Bookmark.objects.filter(title="Archived Bookmark 1").first())
self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 2').first()) self.assertIsNone(Bookmark.objects.filter(title="Archived Bookmark 2").first())
self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 3').first()) self.assertIsNone(Bookmark.objects.filter(title="Archived Bookmark 3").first())
def test_archived_action_bulk_select_across_respects_query(self): def test_archived_action_bulk_select_across_respects_query(self):
self.setup_numbered_bookmarks(3, prefix='foo', archived=True) self.setup_numbered_bookmarks(3, prefix="foo", archived=True)
self.setup_numbered_bookmarks(3, prefix='bar', archived=True) self.setup_numbered_bookmarks(3, prefix="bar", archived=True)
self.assertEqual(3, Bookmark.objects.filter(title__startswith='foo').count()) self.assertEqual(3, Bookmark.objects.filter(title__startswith="foo").count())
self.client.post(reverse('bookmarks:archived.action') + '?q=foo', { self.client.post(
'bulk_action': ['bulk_delete'], reverse("bookmarks:archived.action") + "?q=foo",
'bulk_execute': [''], {
'bulk_select_across': ['on'], "bulk_action": ["bulk_delete"],
}) "bulk_execute": [""],
"bulk_select_across": ["on"],
},
)
self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count()) self.assertEqual(0, Bookmark.objects.filter(title__startswith="foo").count())
self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count()) self.assertEqual(3, Bookmark.objects.filter(title__startswith="bar").count())
def test_shared_action_bulk_select_across_not_supported(self): def test_shared_action_bulk_select_across_not_supported(self):
self.setup_bulk_edit_scope_test_data() self.setup_bulk_edit_scope_test_data()
response = self.client.post(reverse('bookmarks:shared.action'), { response = self.client.post(
'bulk_action': ['bulk_delete'], reverse("bookmarks:shared.action"),
'bulk_execute': [''], {
'bulk_select_across': ['on'], "bulk_action": ["bulk_delete"],
}) "bulk_execute": [""],
"bulk_select_across": ["on"],
},
)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_handles_empty_bookmark_id(self): def test_handles_empty_bookmark_id(self):
@ -541,17 +752,23 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
response = self.client.post(reverse('bookmarks:index.action'), { response = self.client.post(
'bulk_action': ['bulk_archive'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
}) "bulk_action": ["bulk_archive"],
"bulk_execute": [""],
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
response = self.client.post(reverse('bookmarks:index.action'), { response = self.client.post(
'bulk_action': ['bulk_archive'], reverse("bookmarks:index.action"),
'bulk_execute': [''], {
'bookmark_id': [], "bulk_action": ["bulk_archive"],
}) "bulk_execute": [""],
"bookmark_id": [],
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3]) self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
@ -561,9 +778,16 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
self.client.post(reverse('bookmarks:index.action'), { self.client.post(
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], reverse("bookmarks:index.action"),
}) {
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3]) self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
@ -572,14 +796,25 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
url = reverse('bookmarks:index.action') + '?return_url=' + reverse('bookmarks:settings.index') url = (
response = self.client.post(url, { reverse("bookmarks:index.action")
'bulk_action': ['bulk_archive'], + "?return_url="
'bulk_execute': [''], + reverse("bookmarks:settings.index")
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], )
}) response = self.client.post(
url,
{
"bulk_action": ["bulk_archive"],
"bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
)
self.assertRedirects(response, reverse('bookmarks:settings.index')) self.assertRedirects(response, reverse("bookmarks:settings.index"))
def test_should_not_redirect_to_external_url(self): def test_should_not_redirect_to_external_url(self):
bookmark1 = self.setup_bookmark() bookmark1 = self.setup_bookmark()
@ -587,19 +822,27 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
def post_with(return_url, follow=None): def post_with(return_url, follow=None):
url = reverse('bookmarks:index.action') + f'?return_url={return_url}' url = reverse("bookmarks:index.action") + f"?return_url={return_url}"
return self.client.post(url, { return self.client.post(
'bulk_action': ['bulk_archive'], url,
'bulk_execute': [''], {
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], "bulk_action": ["bulk_archive"],
}, follow=follow) "bulk_execute": [""],
"bookmark_id": [
str(bookmark1.id),
str(bookmark2.id),
str(bookmark3.id),
],
},
follow=follow,
)
response = post_with('https://example.com') response = post_with("https://example.com")
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
response = post_with('//example.com') response = post_with("//example.com")
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
response = post_with('://example.com') response = post_with("://example.com")
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
response = post_with('/foo//example.com', follow=True) response = post_with("/foo//example.com", follow=True)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)

View file

@ -6,7 +6,11 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin, collapse_whitespace from bookmarks.tests.helpers import (
BookmarkFactoryMixin,
HtmlTestMixin,
collapse_whitespace,
)
class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin): class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
@ -15,33 +19,41 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)
def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'): def assertVisibleBookmarks(
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]') bookmark_list = soup.select_one(
f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]'
)
self.assertIsNotNone(bookmark_list) self.assertIsNotNone(bookmark_list)
bookmark_items = bookmark_list.select('li[ld-bookmark-item]') bookmark_items = bookmark_list.select("li[ld-bookmark-item]")
self.assertEqual(len(bookmark_items), len(bookmarks)) self.assertEqual(len(bookmark_items), len(bookmarks))
for bookmark in bookmarks: for bookmark in bookmarks:
bookmark_item = bookmark_list.select_one( bookmark_item = bookmark_list.select_one(
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]') f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
)
self.assertIsNotNone(bookmark_item) self.assertIsNotNone(bookmark_item)
def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'): def assertInvisibleBookmarks(
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
for bookmark in bookmarks: for bookmark in bookmarks:
bookmark_item = soup.select_one( bookmark_item = soup.select_one(
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]') f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
)
self.assertIsNone(bookmark_item) self.assertIsNone(bookmark_item)
def assertVisibleTags(self, response, tags: List[Tag]): def assertVisibleTags(self, response, tags: List[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
tag_cloud = soup.select_one('div.tag-cloud') tag_cloud = soup.select_one("div.tag-cloud")
self.assertIsNotNone(tag_cloud) self.assertIsNotNone(tag_cloud)
tag_items = tag_cloud.select('a[data-is-tag-item]') tag_items = tag_cloud.select("a[data-is-tag-item]")
self.assertEqual(len(tag_items), len(tags)) self.assertEqual(len(tag_items), len(tags))
tag_item_names = [tag_item.text.strip() for tag_item in tag_items] tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
@ -51,7 +63,7 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
def assertInvisibleTags(self, response, tags: List[Tag]): def assertInvisibleTags(self, response, tags: List[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
tag_items = soup.select('a[data-is-tag-item]') tag_items = soup.select("a[data-is-tag-item]")
tag_item_names = [tag_item.text.strip() for tag_item in tag_items] tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
@ -60,78 +72,103 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
def assertSelectedTags(self, response, tags: List[Tag]): def assertSelectedTags(self, response, tags: List[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
selected_tags = soup.select_one('p.selected-tags') selected_tags = soup.select_one("p.selected-tags")
self.assertIsNotNone(selected_tags) self.assertIsNotNone(selected_tags)
tag_list = selected_tags.select('a') tag_list = selected_tags.select("a")
self.assertEqual(len(tag_list), len(tags)) self.assertEqual(len(tag_list), len(tags))
for tag in tags: for tag in tags:
self.assertTrue(tag.name in selected_tags.text, msg=f'Selected tags do not contain: {tag.name}') self.assertTrue(
tag.name in selected_tags.text,
msg=f"Selected tags do not contain: {tag.name}",
)
def assertEditLink(self, response, url): def assertEditLink(self, response, url):
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<a href="{url}">Edit</a> <a href="{url}">Edit</a>
''', html) """,
html,
)
def assertBulkActionForm(self, response, url: str): def assertBulkActionForm(self, response, url: str):
html = collapse_whitespace(response.content.decode()) html = collapse_whitespace(response.content.decode())
needle = collapse_whitespace(f''' needle = collapse_whitespace(
f"""
<form class="bookmark-actions" <form class="bookmark-actions"
action="{url}" action="{url}"
method="post" autocomplete="off"> method="post" autocomplete="off">
''') """
)
self.assertIn(needle, html) self.assertIn(needle, html)
def test_should_list_archived_and_user_owned_bookmarks(self): def test_should_list_archived_and_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True) visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
invisible_bookmarks = [ invisible_bookmarks = [
self.setup_bookmark(is_archived=False), self.setup_bookmark(is_archived=False),
self.setup_bookmark(is_archived=True, user=other_user), self.setup_bookmark(is_archived=True, user=other_user),
] ]
response = self.client.get(reverse('bookmarks:archived')) response = self.client.get(reverse("bookmarks:archived"))
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks) self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_bookmarks_matching_query(self): def test_should_list_bookmarks_matching_query(self):
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix='foo', archived=True) visible_bookmarks = self.setup_numbered_bookmarks(
invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix='bar', archived=True) 3, prefix="foo", archived=True
)
invisible_bookmarks = self.setup_numbered_bookmarks(
3, prefix="bar", archived=True
)
response = self.client.get(reverse('bookmarks:archived') + '?q=foo') response = self.client.get(reverse("bookmarks:archived") + "?q=foo")
html = collapse_whitespace(response.content.decode()) html = collapse_whitespace(response.content.decode())
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks) self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_tags_for_archived_and_user_owned_bookmarks(self): def test_should_list_tags_for_archived_and_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True) "otheruser", "otheruser@example.com", "password123"
unarchived_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=False, tag_prefix='unarchived') )
other_user_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, user=other_user, visible_bookmarks = self.setup_numbered_bookmarks(
tag_prefix='otheruser') 3, with_tags=True, archived=True
)
unarchived_bookmarks = self.setup_numbered_bookmarks(
3, with_tags=True, archived=False, tag_prefix="unarchived"
)
other_user_bookmarks = self.setup_numbered_bookmarks(
3, with_tags=True, archived=True, user=other_user, tag_prefix="otheruser"
)
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks) visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
invisible_tags = self.get_tags_from_bookmarks(unarchived_bookmarks + other_user_bookmarks) invisible_tags = self.get_tags_from_bookmarks(
unarchived_bookmarks + other_user_bookmarks
)
response = self.client.get(reverse('bookmarks:archived')) response = self.client.get(reverse("bookmarks:archived"))
self.assertVisibleTags(response, visible_tags) self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags) self.assertInvisibleTags(response, invisible_tags)
def test_should_list_tags_for_bookmarks_matching_query(self): def test_should_list_tags_for_bookmarks_matching_query(self):
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, prefix='foo', visible_bookmarks = self.setup_numbered_bookmarks(
tag_prefix='foo') 3, with_tags=True, archived=True, prefix="foo", tag_prefix="foo"
invisible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, prefix='bar', )
tag_prefix='bar') invisible_bookmarks = self.setup_numbered_bookmarks(
3, with_tags=True, archived=True, prefix="bar", tag_prefix="bar"
)
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks) visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks) invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks)
response = self.client.get(reverse('bookmarks:archived') + '?q=foo') response = self.client.get(reverse("bookmarks:archived") + "?q=foo")
self.assertVisibleTags(response, visible_tags) self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags) self.assertInvisibleTags(response, invisible_tags)
@ -139,19 +176,31 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
def test_should_list_bookmarks_and_tags_for_search_preferences(self): def test_should_list_bookmarks_and_tags_for_search_preferences(self):
user_profile = self.user.profile user_profile = self.user.profile
user_profile.search_preferences = { user_profile.search_preferences = {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "unread": BookmarkSearch.FILTER_UNREAD_YES,
} }
user_profile.save() user_profile.save()
unread_bookmarks = self.setup_numbered_bookmarks(3, archived=True, unread=True, with_tags=True, prefix='unread', unread_bookmarks = self.setup_numbered_bookmarks(
tag_prefix='unread') 3,
read_bookmarks = self.setup_numbered_bookmarks(3, archived=True, unread=False, with_tags=True, prefix='read', archived=True,
tag_prefix='read') unread=True,
with_tags=True,
prefix="unread",
tag_prefix="unread",
)
read_bookmarks = self.setup_numbered_bookmarks(
3,
archived=True,
unread=False,
with_tags=True,
prefix="read",
tag_prefix="read",
)
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks) unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
read_tags = self.get_tags_from_bookmarks(read_bookmarks) read_tags = self.get_tags_from_bookmarks(read_bookmarks)
response = self.client.get(reverse('bookmarks:archived')) response = self.client.get(reverse("bookmarks:archived"))
self.assertVisibleBookmarks(response, unread_bookmarks) self.assertVisibleBookmarks(response, unread_bookmarks)
self.assertInvisibleBookmarks(response, read_bookmarks) self.assertInvisibleBookmarks(response, read_bookmarks)
self.assertVisibleTags(response, unread_tags) self.assertVisibleTags(response, unread_tags)
@ -167,11 +216,15 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
] ]
self.setup_bookmark(is_archived=True, tags=tags) 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}') response = self.client.get(
reverse("bookmarks:archived") + f"?q=%23{tags[0].name}+%23{tags[1].name}"
)
self.assertSelectedTags(response, [tags[0], tags[1]]) self.assertSelectedTags(response, [tags[0], tags[1]])
def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(self): def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(
self,
):
tags = [ tags = [
self.setup_tag(), self.setup_tag(),
self.setup_tag(), self.setup_tag(),
@ -181,7 +234,10 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
] ]
self.setup_bookmark(title=tags[0].name, tags=tags, is_archived=True) self.setup_bookmark(title=tags[0].name, tags=tags, is_archived=True)
response = self.client.get(reverse('bookmarks:archived') + f'?q={tags[0].name}+%23{tags[1].name.upper()}') response = self.client.get(
reverse("bookmarks:archived")
+ f"?q={tags[0].name}+%23{tags[1].name.upper()}"
)
self.assertSelectedTags(response, [tags[1]]) self.assertSelectedTags(response, [tags[1]])
@ -198,16 +254,19 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
] ]
self.setup_bookmark(tags=tags, is_archived=True) self.setup_bookmark(tags=tags, is_archived=True)
response = self.client.get(reverse('bookmarks:archived') + f'?q={tags[0].name}+%23{tags[1].name.upper()}') response = self.client.get(
reverse("bookmarks:archived")
+ f"?q={tags[0].name}+%23{tags[1].name.upper()}"
)
self.assertSelectedTags(response, [tags[0], tags[1]]) self.assertSelectedTags(response, [tags[0], tags[1]])
def test_should_open_bookmarks_in_new_page_by_default(self): def test_should_open_bookmarks_in_new_page_by_default(self):
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True) visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
response = self.client.get(reverse('bookmarks:archived')) response = self.client.get(reverse("bookmarks:archived"))
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank') self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self): def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
@ -216,71 +275,72 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True) visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
response = self.client.get(reverse('bookmarks:archived')) response = self.client.get(reverse("bookmarks:archived"))
self.assertVisibleBookmarks(response, visible_bookmarks, '_self') self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
def test_edit_link_return_url_respects_search_options(self): def test_edit_link_return_url_respects_search_options(self):
bookmark = self.setup_bookmark(title='foo', is_archived=True) bookmark = self.setup_bookmark(title="foo", is_archived=True)
edit_url = reverse('bookmarks:edit', args=[bookmark.id]) edit_url = reverse("bookmarks:edit", args=[bookmark.id])
base_url = reverse('bookmarks:archived') base_url = reverse("bookmarks:archived")
# without query params # without query params
return_url = urllib.parse.quote(base_url) return_url = urllib.parse.quote(base_url)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url) response = self.client.get(base_url)
self.assertEditLink(response, url) self.assertEditLink(response, url)
# with query # with query
url_params = '?q=foo' url_params = "?q=foo"
return_url = urllib.parse.quote(base_url + url_params) return_url = urllib.parse.quote(base_url + url_params)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertEditLink(response, url) self.assertEditLink(response, url)
# with query and sort and page # with query and sort and page
url_params = '?q=foo&sort=title_asc&page=2' url_params = "?q=foo&sort=title_asc&page=2"
return_url = urllib.parse.quote(base_url + url_params) return_url = urllib.parse.quote(base_url + url_params)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertEditLink(response, url) self.assertEditLink(response, url)
def test_bulk_edit_respects_search_options(self): def test_bulk_edit_respects_search_options(self):
action_url = reverse('bookmarks:archived.action') action_url = reverse("bookmarks:archived.action")
base_url = reverse('bookmarks:archived') base_url = reverse("bookmarks:archived")
# without params # without params
return_url = urllib.parse.quote_plus(base_url) return_url = urllib.parse.quote_plus(base_url)
url = f'{action_url}?return_url={return_url}' url = f"{action_url}?return_url={return_url}"
response = self.client.get(base_url) response = self.client.get(base_url)
self.assertBulkActionForm(response, url) self.assertBulkActionForm(response, url)
# with query # with query
url_params = '?q=foo' url_params = "?q=foo"
return_url = urllib.parse.quote_plus(base_url + url_params) return_url = urllib.parse.quote_plus(base_url + url_params)
url = f'{action_url}?q=foo&return_url={return_url}' url = f"{action_url}?q=foo&return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertBulkActionForm(response, url) self.assertBulkActionForm(response, url)
# with query and sort # with query and sort
url_params = '?q=foo&sort=title_asc' url_params = "?q=foo&sort=title_asc"
return_url = urllib.parse.quote_plus(base_url + url_params) return_url = urllib.parse.quote_plus(base_url + url_params)
url = f'{action_url}?q=foo&sort=title_asc&return_url={return_url}' url = f"{action_url}?q=foo&sort=title_asc&return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertBulkActionForm(response, url) self.assertBulkActionForm(response, url)
def test_allowed_bulk_actions(self): def test_allowed_bulk_actions(self):
url = reverse('bookmarks:archived') url = reverse("bookmarks:archived")
response = self.client.get(url) response = self.client.get(url)
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_unarchive">Unarchive</option> <option value="bulk_unarchive">Unarchive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@ -289,18 +349,21 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
<option value="bulk_read">Mark as read</option> <option value="bulk_read">Mark as read</option>
<option value="bulk_unread">Mark as unread</option> <option value="bulk_unread">Mark as unread</option>
</select> </select>
''', html) """,
html,
)
def test_allowed_bulk_actions_with_sharing_enabled(self): def test_allowed_bulk_actions_with_sharing_enabled(self):
user_profile = self.user.profile user_profile = self.user.profile
user_profile.enable_sharing = True user_profile.enable_sharing = True
user_profile.save() user_profile.save()
url = reverse('bookmarks:archived') url = reverse("bookmarks:archived")
response = self.client.get(url) response = self.client.get(url)
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_unarchive">Unarchive</option> <option value="bulk_unarchive">Unarchive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@ -311,142 +374,191 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
<option value="bulk_share">Share</option> <option value="bulk_share">Share</option>
<option value="bulk_unshare">Unshare</option> <option value="bulk_unshare">Unshare</option>
</select> </select>
''', html) """,
html,
)
def test_apply_search_preferences(self): def test_apply_search_preferences(self):
# no params # no params
response = self.client.post(reverse('bookmarks:archived')) response = self.client.post(reverse("bookmarks:archived"))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:archived')) self.assertEqual(response.url, reverse("bookmarks:archived"))
# some params # some params
response = self.client.post(reverse('bookmarks:archived'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:archived"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
}) "q": "foo",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&sort=title_asc') self.assertEqual(
response.url, reverse("bookmarks:archived") + "?q=foo&sort=title_asc"
)
# params with default value are removed # params with default value are removed
response = self.client.post(reverse('bookmarks:archived'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:archived"),
'user': '', {
'sort': BookmarkSearch.SORT_ADDED_DESC, "q": "foo",
'shared': BookmarkSearch.FILTER_SHARED_OFF, "user": "",
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&unread=yes') self.assertEqual(
response.url, reverse("bookmarks:archived") + "?q=foo&unread=yes"
)
# page is removed # page is removed
response = self.client.post(reverse('bookmarks:archived'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:archived"),
'page': '2', {
'sort': BookmarkSearch.SORT_TITLE_ASC, "q": "foo",
}) "page": "2",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&sort=title_asc') self.assertEqual(
response.url, reverse("bookmarks:archived") + "?q=foo&sort=title_asc"
)
def test_save_search_preferences(self): def test_save_search_preferences(self):
user_profile = self.user.profile user_profile = self.user.profile
# no params # no params
self.client.post(reverse('bookmarks:archived'), { self.client.post(
'save': '', reverse("bookmarks:archived"),
}) {
"save": "",
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_ADDED_DESC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
# with param # with param
self.client.post(reverse('bookmarks:archived'), { self.client.post(
'save': '', reverse("bookmarks:archived"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
}) "save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
# add a param # add a param
self.client.post(reverse('bookmarks:archived'), { self.client.post(
'save': '', reverse("bookmarks:archived"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "save": "",
}) "sort": BookmarkSearch.SORT_TITLE_ASC,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
# remove a param # remove a param
self.client.post(reverse('bookmarks:archived'), { self.client.post(
'save': '', reverse("bookmarks:archived"),
'unread': BookmarkSearch.FILTER_UNREAD_YES, {
}) "save": "",
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_ADDED_DESC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
# ignores non-preferences # ignores non-preferences
self.client.post(reverse('bookmarks:archived'), { self.client.post(
'save': '', reverse("bookmarks:archived"),
'q': 'foo', {
'user': 'john', "save": "",
'page': '3', "q": "foo",
'sort': BookmarkSearch.SORT_TITLE_ASC, "user": "john",
}) "page": "3",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
def test_url_encode_bookmark_actions_url(self): def test_url_encode_bookmark_actions_url(self):
url = reverse('bookmarks:archived') + '?q=%23foo' url = reverse("bookmarks:archived") + "?q=%23foo"
response = self.client.get(url) response = self.client.get(url)
html = response.content.decode() html = response.content.decode()
soup = self.make_soup(html) soup = self.make_soup(html)
actions_form = soup.select('form.bookmark-actions')[0] actions_form = soup.select("form.bookmark-actions")[0]
self.assertEqual(actions_form.attrs['action'], self.assertEqual(
'/bookmarks/archived/action?q=%23foo&return_url=%2Fbookmarks%2Farchived%3Fq%3D%2523foo') actions_form.attrs["action"],
"/bookmarks/archived/action?q=%23foo&return_url=%2Fbookmarks%2Farchived%3Fq%3D%2523foo",
)
def test_encode_search_params(self): def test_encode_search_params(self):
bookmark = self.setup_bookmark(description='alert(\'xss\')', is_archived=True) bookmark = self.setup_bookmark(description="alert('xss')", is_archived=True)
url = reverse('bookmarks:archived') + '?q=alert(%27xss%27)' url = reverse("bookmarks:archived") + "?q=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
self.assertContains(response, bookmark.url) self.assertContains(response, bookmark.url)
url = reverse('bookmarks:archived') + '?sort=alert(%27xss%27)' url = reverse("bookmarks:archived") + "?sort=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:archived') + '?unread=alert(%27xss%27)' url = reverse("bookmarks:archived") + "?unread=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:archived') + '?shared=alert(%27xss%27)' url = reverse("bookmarks:archived") + "?shared=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:archived') + '?user=alert(%27xss%27)' url = reverse("bookmarks:archived") + "?user=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:archived') + '?page=alert(%27xss%27)' url = reverse("bookmarks:archived") + "?page=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")

View file

@ -8,7 +8,9 @@ from django.db.utils import DEFAULT_DB_ALIAS
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryMixin): class BookmarkArchivedViewPerformanceTestCase(
TransactionTestCase, BookmarkFactoryMixin
):
def setUp(self) -> None: def setUp(self) -> None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
@ -26,8 +28,10 @@ class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFacto
# capture number of queries # capture number of queries
context = CaptureQueriesContext(self.get_connection()) context = CaptureQueriesContext(self.get_connection())
with context: with context:
response = self.client.get(reverse('bookmarks:archived')) response = self.client.get(reverse("bookmarks:archived"))
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks) self.assertContains(
response, "<li ld-bookmark-item>", num_initial_bookmarks
)
number_of_queries = context.final_queries number_of_queries = context.final_queries
@ -38,5 +42,9 @@ class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFacto
# assert num queries doesn't increase # assert num queries doesn't increase
with self.assertNumQueries(number_of_queries): with self.assertNumQueries(number_of_queries):
response = self.client.get(reverse('bookmarks:archived')) response = self.client.get(reverse("bookmarks:archived"))
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks + num_additional_bookmarks) self.assertContains(
response,
"<li ld-bookmark-item>",
num_initial_bookmarks + num_additional_bookmarks,
)

View file

@ -16,153 +16,192 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
if overrides is None: if overrides is None:
overrides = {} overrides = {}
form_data = { form_data = {
'url': 'http://example.com/edited', "url": "http://example.com/edited",
'tag_string': 'editedtag1 editedtag2', "tag_string": "editedtag1 editedtag2",
'title': 'edited title', "title": "edited title",
'description': 'edited description', "description": "edited description",
'notes': 'edited notes', "notes": "edited notes",
'unread': False, "unread": False,
'shared': False, "shared": False,
} }
return {**form_data, **overrides} return {**form_data, **overrides}
def test_should_edit_bookmark(self): def test_should_edit_bookmark(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
form_data = self.create_form_data({'id': bookmark.id}) form_data = self.create_form_data({"id": bookmark.id})
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data) self.client.post(reverse("bookmarks:edit", args=[bookmark.id]), form_data)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual(bookmark.owner, self.user) self.assertEqual(bookmark.owner, self.user)
self.assertEqual(bookmark.url, form_data['url']) self.assertEqual(bookmark.url, form_data["url"])
self.assertEqual(bookmark.title, form_data['title']) self.assertEqual(bookmark.title, form_data["title"])
self.assertEqual(bookmark.description, form_data['description']) self.assertEqual(bookmark.description, form_data["description"])
self.assertEqual(bookmark.notes, form_data['notes']) self.assertEqual(bookmark.notes, form_data["notes"])
self.assertEqual(bookmark.unread, form_data['unread']) self.assertEqual(bookmark.unread, form_data["unread"])
self.assertEqual(bookmark.shared, form_data['shared']) self.assertEqual(bookmark.shared, form_data["shared"])
self.assertEqual(bookmark.tags.count(), 2) self.assertEqual(bookmark.tags.count(), 2)
tags = bookmark.tags.order_by('name').all() tags = bookmark.tags.order_by("name").all()
self.assertEqual(tags[0].name, 'editedtag1') self.assertEqual(tags[0].name, "editedtag1")
self.assertEqual(tags[1].name, 'editedtag2') self.assertEqual(tags[1].name, "editedtag2")
def test_should_edit_unread_state(self): def test_should_edit_unread_state(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
form_data = self.create_form_data({'id': bookmark.id, 'unread': True}) 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("bookmarks:edit", args=[bookmark.id]), form_data)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertTrue(bookmark.unread) self.assertTrue(bookmark.unread)
form_data = self.create_form_data({'id': bookmark.id, 'unread': False}) 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("bookmarks:edit", args=[bookmark.id]), form_data)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertFalse(bookmark.unread) self.assertFalse(bookmark.unread)
def test_should_edit_shared_state(self): def test_should_edit_shared_state(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
form_data = self.create_form_data({'id': bookmark.id, 'shared': True}) 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("bookmarks:edit", args=[bookmark.id]), form_data)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertTrue(bookmark.shared) self.assertTrue(bookmark.shared)
form_data = self.create_form_data({'id': bookmark.id, 'shared': False}) 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("bookmarks:edit", args=[bookmark.id]), form_data)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertFalse(bookmark.shared) self.assertFalse(bookmark.shared)
def test_should_prefill_bookmark_form_fields(self): def test_should_prefill_bookmark_form_fields(self):
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
bookmark = self.setup_bookmark(tags=[tag1, tag2], title='edited title', description='edited description', bookmark = self.setup_bookmark(
notes='edited notes', website_title='website title', tags=[tag1, tag2],
website_description='website description') title="edited title",
description="edited description",
notes="edited notes",
website_title="website title",
website_description="website description",
)
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id])) response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<input type="text" name="url" value="{bookmark.url}" placeholder=" " <input type="text" name="url" value="{bookmark.url}" placeholder=" "
autofocus class="form-input" required id="id_url"> autofocus class="form-input" required id="id_url">
''', html) """,
html,
)
tag_string = build_tag_string(bookmark.tag_names, ' ') tag_string = build_tag_string(bookmark.tag_names, " ")
self.assertInHTML(f''' self.assertInHTML(
f"""
<input ld-tag-autocomplete type="text" name="tag_string" value="{tag_string}" <input ld-tag-autocomplete type="text" name="tag_string" value="{tag_string}"
autocomplete="off" autocapitalize="off" class="form-input" id="id_tag_string"> autocomplete="off" autocapitalize="off" class="form-input" id="id_tag_string">
''', html) """,
html,
)
self.assertInHTML(f''' self.assertInHTML(
f"""
<input type="text" name="title" value="{bookmark.title}" maxlength="512" autocomplete="off" <input type="text" name="title" value="{bookmark.title}" maxlength="512" autocomplete="off"
class="form-input" id="id_title"> class="form-input" id="id_title">
''', html) """,
html,
)
self.assertInHTML(f''' self.assertInHTML(
f"""
<textarea name="description" cols="40" rows="2" class="form-input" id="id_description"> <textarea name="description" cols="40" rows="2" class="form-input" id="id_description">
{bookmark.description} {bookmark.description}
</textarea> </textarea>
''', html) """,
html,
)
self.assertInHTML(f''' self.assertInHTML(
f"""
<textarea name="notes" cols="40" rows="8" class="form-input" id="id_notes"> <textarea name="notes" cols="40" rows="8" class="form-input" id="id_notes">
{bookmark.notes} {bookmark.notes}
</textarea> </textarea>
''', html) """,
html,
)
self.assertInHTML(f''' self.assertInHTML(
f"""
<input type="hidden" name="website_title" id="id_website_title" <input type="hidden" name="website_title" id="id_website_title"
value="{bookmark.website_title}"> value="{bookmark.website_title}">
''', html) """,
html,
)
self.assertInHTML(f''' self.assertInHTML(
f"""
<input type="hidden" name="website_description" id="id_website_description" <input type="hidden" name="website_description" id="id_website_description"
value="{bookmark.website_description}"> value="{bookmark.website_description}">
''', html) """,
html,
)
def test_should_redirect_to_return_url(self): def test_should_redirect_to_return_url(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
form_data = self.create_form_data() form_data = self.create_form_data()
url = reverse('bookmarks:edit', args=[bookmark.id]) + '?return_url=' + reverse('bookmarks:close') url = (
reverse("bookmarks:edit", args=[bookmark.id])
+ "?return_url="
+ reverse("bookmarks:close")
)
response = self.client.post(url, form_data) response = self.client.post(url, form_data)
self.assertRedirects(response, reverse('bookmarks:close')) self.assertRedirects(response, reverse("bookmarks:close"))
def test_should_redirect_to_bookmark_index_by_default(self): def test_should_redirect_to_bookmark_index_by_default(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
form_data = self.create_form_data() form_data = self.create_form_data()
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data) response = self.client.post(
reverse("bookmarks:edit", args=[bookmark.id]), form_data
)
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
def test_should_not_redirect_to_external_url(self): def test_should_not_redirect_to_external_url(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
def post_with(return_url, follow=None): def post_with(return_url, follow=None):
form_data = self.create_form_data() form_data = self.create_form_data()
url = reverse('bookmarks:edit', args=[bookmark.id]) + f'?return_url={return_url}' url = (
reverse("bookmarks:edit", args=[bookmark.id])
+ f"?return_url={return_url}"
)
return self.client.post(url, form_data, follow=follow) return self.client.post(url, form_data, follow=follow)
response = post_with('https://example.com') response = post_with("https://example.com")
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
response = post_with('//example.com') response = post_with("//example.com")
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
response = post_with('://example.com') response = post_with("://example.com")
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
response = post_with('/foo//example.com', follow=True) response = post_with("/foo//example.com", follow=True)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_can_only_edit_own_bookmarks(self): def test_can_only_edit_own_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark = self.setup_bookmark(user=other_user) bookmark = self.setup_bookmark(user=other_user)
form_data = self.create_form_data({'id': bookmark.id}) form_data = self.create_form_data({"id": bookmark.id})
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data) response = self.client.post(
reverse("bookmarks:edit", args=[bookmark.id]), form_data
)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertNotEqual(bookmark.url, form_data['url']) self.assertNotEqual(bookmark.url, form_data["url"])
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_should_respect_share_profile_setting(self): def test_should_respect_share_profile_setting(self):
@ -170,38 +209,46 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
self.user.profile.enable_sharing = False self.user.profile.enable_sharing = False
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id])) response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(''' self.assertInHTML(
"""
<label for="id_shared" class="form-checkbox"> <label for="id_shared" class="form-checkbox">
<input type="checkbox" name="shared" id="id_shared"> <input type="checkbox" name="shared" id="id_shared">
<i class="form-icon"></i> <i class="form-icon"></i>
<span>Share</span> <span>Share</span>
</label> </label>
''', html, count=0) """,
html,
count=0,
)
self.user.profile.enable_sharing = True self.user.profile.enable_sharing = True
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id])) response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(''' self.assertInHTML(
"""
<label for="id_shared" class="form-checkbox"> <label for="id_shared" class="form-checkbox">
<input type="checkbox" name="shared" id="id_shared"> <input type="checkbox" name="shared" id="id_shared">
<i class="form-icon"></i> <i class="form-icon"></i>
<span>Share</span> <span>Share</span>
</label> </label>
''', html, count=1) """,
html,
count=1,
)
def test_should_hide_notes_if_there_are_no_notes(self): def test_should_hide_notes_if_there_are_no_notes(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id])) response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
self.assertContains(response, '<details class="notes">', count=1) self.assertContains(response, '<details class="notes">', count=1)
def test_should_show_notes_if_there_are_notes(self): def test_should_show_notes_if_there_are_notes(self):
bookmark = self.setup_bookmark(notes='test notes') bookmark = self.setup_bookmark(notes="test notes")
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id])) response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
self.assertContains(response, '<details class="notes" open>', count=1) self.assertContains(response, '<details class="notes" open>', count=1)

View file

@ -6,7 +6,11 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin, collapse_whitespace from bookmarks.tests.helpers import (
BookmarkFactoryMixin,
HtmlTestMixin,
collapse_whitespace,
)
class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin): class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
@ -15,33 +19,41 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)
def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'): def assertVisibleBookmarks(
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]') bookmark_list = soup.select_one(
f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]'
)
self.assertIsNotNone(bookmark_list) self.assertIsNotNone(bookmark_list)
bookmark_items = bookmark_list.select('li[ld-bookmark-item]') bookmark_items = bookmark_list.select("li[ld-bookmark-item]")
self.assertEqual(len(bookmark_items), len(bookmarks)) self.assertEqual(len(bookmark_items), len(bookmarks))
for bookmark in bookmarks: for bookmark in bookmarks:
bookmark_item = bookmark_list.select_one( bookmark_item = bookmark_list.select_one(
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]') f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
)
self.assertIsNotNone(bookmark_item) self.assertIsNotNone(bookmark_item)
def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'): def assertInvisibleBookmarks(
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
for bookmark in bookmarks: for bookmark in bookmarks:
bookmark_item = soup.select_one( bookmark_item = soup.select_one(
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]') f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
)
self.assertIsNone(bookmark_item) self.assertIsNone(bookmark_item)
def assertVisibleTags(self, response, tags: List[Tag]): def assertVisibleTags(self, response, tags: List[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
tag_cloud = soup.select_one('div.tag-cloud') tag_cloud = soup.select_one("div.tag-cloud")
self.assertIsNotNone(tag_cloud) self.assertIsNotNone(tag_cloud)
tag_items = tag_cloud.select('a[data-is-tag-item]') tag_items = tag_cloud.select("a[data-is-tag-item]")
self.assertEqual(len(tag_items), len(tags)) self.assertEqual(len(tag_items), len(tags))
tag_item_names = [tag_item.text.strip() for tag_item in tag_items] tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
@ -51,7 +63,7 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def assertInvisibleTags(self, response, tags: List[Tag]): def assertInvisibleTags(self, response, tags: List[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
tag_items = soup.select('a[data-is-tag-item]') tag_items = soup.select("a[data-is-tag-item]")
tag_item_names = [tag_item.text.strip() for tag_item in tag_items] tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
@ -60,74 +72,96 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def assertSelectedTags(self, response, tags: List[Tag]): def assertSelectedTags(self, response, tags: List[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
selected_tags = soup.select_one('p.selected-tags') selected_tags = soup.select_one("p.selected-tags")
self.assertIsNotNone(selected_tags) self.assertIsNotNone(selected_tags)
tag_list = selected_tags.select('a') tag_list = selected_tags.select("a")
self.assertEqual(len(tag_list), len(tags)) self.assertEqual(len(tag_list), len(tags))
for tag in tags: for tag in tags:
self.assertTrue(tag.name in selected_tags.text, msg=f'Selected tags do not contain: {tag.name}') self.assertTrue(
tag.name in selected_tags.text,
msg=f"Selected tags do not contain: {tag.name}",
)
def assertEditLink(self, response, url): def assertEditLink(self, response, url):
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<a href="{url}">Edit</a> <a href="{url}">Edit</a>
''', html) """,
html,
)
def assertBulkActionForm(self, response, url: str): def assertBulkActionForm(self, response, url: str):
html = collapse_whitespace(response.content.decode()) html = collapse_whitespace(response.content.decode())
needle = collapse_whitespace(f''' needle = collapse_whitespace(
f"""
<form class="bookmark-actions" <form class="bookmark-actions"
action="{url}" action="{url}"
method="post" autocomplete="off"> method="post" autocomplete="off">
''') """
)
self.assertIn(needle, html) self.assertIn(needle, html)
def test_should_list_unarchived_and_user_owned_bookmarks(self): def test_should_list_unarchived_and_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
visible_bookmarks = self.setup_numbered_bookmarks(3) visible_bookmarks = self.setup_numbered_bookmarks(3)
invisible_bookmarks = [ invisible_bookmarks = [
self.setup_bookmark(is_archived=True), self.setup_bookmark(is_archived=True),
self.setup_bookmark(user=other_user), self.setup_bookmark(user=other_user),
] ]
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks) self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_bookmarks_matching_query(self): def test_should_list_bookmarks_matching_query(self):
visible_bookmarks = self.setup_numbered_bookmarks(3, prefix='foo') visible_bookmarks = self.setup_numbered_bookmarks(3, prefix="foo")
invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix='bar') invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix="bar")
response = self.client.get(reverse('bookmarks:index') + '?q=foo') response = self.client.get(reverse("bookmarks:index") + "?q=foo")
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks) self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_tags_for_unarchived_and_user_owned_bookmarks(self): def test_should_list_tags_for_unarchived_and_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True) visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True)
archived_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, tag_prefix='archived') archived_bookmarks = self.setup_numbered_bookmarks(
other_user_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, user=other_user, tag_prefix='otheruser') 3, with_tags=True, archived=True, tag_prefix="archived"
)
other_user_bookmarks = self.setup_numbered_bookmarks(
3, with_tags=True, user=other_user, tag_prefix="otheruser"
)
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks) visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
invisible_tags = self.get_tags_from_bookmarks(archived_bookmarks + other_user_bookmarks) invisible_tags = self.get_tags_from_bookmarks(
archived_bookmarks + other_user_bookmarks
)
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
self.assertVisibleTags(response, visible_tags) self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags) self.assertInvisibleTags(response, invisible_tags)
def test_should_list_tags_for_bookmarks_matching_query(self): def test_should_list_tags_for_bookmarks_matching_query(self):
visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, prefix='foo', tag_prefix='foo') visible_bookmarks = self.setup_numbered_bookmarks(
invisible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, prefix='bar', tag_prefix='bar') 3, with_tags=True, prefix="foo", tag_prefix="foo"
)
invisible_bookmarks = self.setup_numbered_bookmarks(
3, with_tags=True, prefix="bar", tag_prefix="bar"
)
visible_tags = self.get_tags_from_bookmarks(visible_bookmarks) visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks) invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks)
response = self.client.get(reverse('bookmarks:index') + '?q=foo') response = self.client.get(reverse("bookmarks:index") + "?q=foo")
self.assertVisibleTags(response, visible_tags) self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags) self.assertInvisibleTags(response, invisible_tags)
@ -135,19 +169,21 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def test_should_list_bookmarks_and_tags_for_search_preferences(self): def test_should_list_bookmarks_and_tags_for_search_preferences(self):
user_profile = self.user.profile user_profile = self.user.profile
user_profile.search_preferences = { user_profile.search_preferences = {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "unread": BookmarkSearch.FILTER_UNREAD_YES,
} }
user_profile.save() user_profile.save()
unread_bookmarks = self.setup_numbered_bookmarks(3, unread=True, with_tags=True, prefix='unread', unread_bookmarks = self.setup_numbered_bookmarks(
tag_prefix='unread') 3, unread=True, with_tags=True, prefix="unread", tag_prefix="unread"
read_bookmarks = self.setup_numbered_bookmarks(3, unread=False, with_tags=True, prefix='read', )
tag_prefix='read') read_bookmarks = self.setup_numbered_bookmarks(
3, unread=False, with_tags=True, prefix="read", tag_prefix="read"
)
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks) unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
read_tags = self.get_tags_from_bookmarks(read_bookmarks) read_tags = self.get_tags_from_bookmarks(read_bookmarks)
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
self.assertVisibleBookmarks(response, unread_bookmarks) self.assertVisibleBookmarks(response, unread_bookmarks)
self.assertInvisibleBookmarks(response, read_bookmarks) self.assertInvisibleBookmarks(response, read_bookmarks)
self.assertVisibleTags(response, unread_tags) self.assertVisibleTags(response, unread_tags)
@ -163,11 +199,16 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
] ]
self.setup_bookmark(tags=tags) self.setup_bookmark(tags=tags)
response = self.client.get(reverse('bookmarks:index') + f'?q=%23{tags[0].name}+%23{tags[1].name.upper()}') response = self.client.get(
reverse("bookmarks:index")
+ f"?q=%23{tags[0].name}+%23{tags[1].name.upper()}"
)
self.assertSelectedTags(response, [tags[0], tags[1]]) self.assertSelectedTags(response, [tags[0], tags[1]])
def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(self): def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(
self,
):
tags = [ tags = [
self.setup_tag(), self.setup_tag(),
self.setup_tag(), self.setup_tag(),
@ -177,7 +218,9 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
] ]
self.setup_bookmark(title=tags[0].name, tags=tags) 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()}') response = self.client.get(
reverse("bookmarks:index") + f"?q={tags[0].name}+%23{tags[1].name.upper()}"
)
self.assertSelectedTags(response, [tags[1]]) self.assertSelectedTags(response, [tags[1]])
@ -194,16 +237,18 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
] ]
self.setup_bookmark(tags=tags) self.setup_bookmark(tags=tags)
response = self.client.get(reverse('bookmarks:index') + f'?q={tags[0].name}+%23{tags[1].name.upper()}') response = self.client.get(
reverse("bookmarks:index") + f"?q={tags[0].name}+%23{tags[1].name.upper()}"
)
self.assertSelectedTags(response, [tags[0], tags[1]]) self.assertSelectedTags(response, [tags[0], tags[1]])
def test_should_open_bookmarks_in_new_page_by_default(self): def test_should_open_bookmarks_in_new_page_by_default(self):
visible_bookmarks = self.setup_numbered_bookmarks(3) visible_bookmarks = self.setup_numbered_bookmarks(3)
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank') self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self): def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
@ -212,71 +257,72 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
visible_bookmarks = self.setup_numbered_bookmarks(3) visible_bookmarks = self.setup_numbered_bookmarks(3)
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
self.assertVisibleBookmarks(response, visible_bookmarks, '_self') self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
def test_edit_link_return_url_respects_search_options(self): def test_edit_link_return_url_respects_search_options(self):
bookmark = self.setup_bookmark(title='foo') bookmark = self.setup_bookmark(title="foo")
edit_url = reverse('bookmarks:edit', args=[bookmark.id]) edit_url = reverse("bookmarks:edit", args=[bookmark.id])
base_url = reverse('bookmarks:index') base_url = reverse("bookmarks:index")
# without query params # without query params
return_url = urllib.parse.quote(base_url) return_url = urllib.parse.quote(base_url)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url) response = self.client.get(base_url)
self.assertEditLink(response, url) self.assertEditLink(response, url)
# with query # with query
url_params = '?q=foo' url_params = "?q=foo"
return_url = urllib.parse.quote(base_url + url_params) return_url = urllib.parse.quote(base_url + url_params)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertEditLink(response, url) self.assertEditLink(response, url)
# with query and sort and page # with query and sort and page
url_params = '?q=foo&sort=title_asc&page=2' url_params = "?q=foo&sort=title_asc&page=2"
return_url = urllib.parse.quote(base_url + url_params) return_url = urllib.parse.quote(base_url + url_params)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertEditLink(response, url) self.assertEditLink(response, url)
def test_bulk_edit_respects_search_options(self): def test_bulk_edit_respects_search_options(self):
action_url = reverse('bookmarks:index.action') action_url = reverse("bookmarks:index.action")
base_url = reverse('bookmarks:index') base_url = reverse("bookmarks:index")
# without params # without params
return_url = urllib.parse.quote_plus(base_url) return_url = urllib.parse.quote_plus(base_url)
url = f'{action_url}?return_url={return_url}' url = f"{action_url}?return_url={return_url}"
response = self.client.get(base_url) response = self.client.get(base_url)
self.assertBulkActionForm(response, url) self.assertBulkActionForm(response, url)
# with query # with query
url_params = '?q=foo' url_params = "?q=foo"
return_url = urllib.parse.quote_plus(base_url + url_params) return_url = urllib.parse.quote_plus(base_url + url_params)
url = f'{action_url}?q=foo&return_url={return_url}' url = f"{action_url}?q=foo&return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertBulkActionForm(response, url) self.assertBulkActionForm(response, url)
# with query and sort # with query and sort
url_params = '?q=foo&sort=title_asc' url_params = "?q=foo&sort=title_asc"
return_url = urllib.parse.quote_plus(base_url + url_params) return_url = urllib.parse.quote_plus(base_url + url_params)
url = f'{action_url}?q=foo&sort=title_asc&return_url={return_url}' url = f"{action_url}?q=foo&sort=title_asc&return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertBulkActionForm(response, url) self.assertBulkActionForm(response, url)
def test_allowed_bulk_actions(self): def test_allowed_bulk_actions(self):
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
response = self.client.get(url) response = self.client.get(url)
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_archive">Archive</option> <option value="bulk_archive">Archive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@ -285,18 +331,21 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
<option value="bulk_read">Mark as read</option> <option value="bulk_read">Mark as read</option>
<option value="bulk_unread">Mark as unread</option> <option value="bulk_unread">Mark as unread</option>
</select> </select>
''', html) """,
html,
)
def test_allowed_bulk_actions_with_sharing_enabled(self): def test_allowed_bulk_actions_with_sharing_enabled(self):
user_profile = self.user.profile user_profile = self.user.profile
user_profile.enable_sharing = True user_profile.enable_sharing = True
user_profile.save() user_profile.save()
url = reverse('bookmarks:index') url = reverse("bookmarks:index")
response = self.client.get(url) response = self.client.get(url)
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
<option value="bulk_archive">Archive</option> <option value="bulk_archive">Archive</option>
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
@ -307,142 +356,189 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
<option value="bulk_share">Share</option> <option value="bulk_share">Share</option>
<option value="bulk_unshare">Unshare</option> <option value="bulk_unshare">Unshare</option>
</select> </select>
''', html) """,
html,
)
def test_apply_search_preferences(self): def test_apply_search_preferences(self):
# no params # no params
response = self.client.post(reverse('bookmarks:index')) response = self.client.post(reverse("bookmarks:index"))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:index')) self.assertEqual(response.url, reverse("bookmarks:index"))
# some params # some params
response = self.client.post(reverse('bookmarks:index'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:index"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
}) "q": "foo",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&sort=title_asc') self.assertEqual(
response.url, reverse("bookmarks:index") + "?q=foo&sort=title_asc"
)
# params with default value are removed # params with default value are removed
response = self.client.post(reverse('bookmarks:index'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:index"),
'user': '', {
'sort': BookmarkSearch.SORT_ADDED_DESC, "q": "foo",
'shared': BookmarkSearch.FILTER_SHARED_OFF, "user": "",
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&unread=yes') self.assertEqual(response.url, reverse("bookmarks:index") + "?q=foo&unread=yes")
# page is removed # page is removed
response = self.client.post(reverse('bookmarks:index'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:index"),
'page': '2', {
'sort': BookmarkSearch.SORT_TITLE_ASC, "q": "foo",
}) "page": "2",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&sort=title_asc') self.assertEqual(
response.url, reverse("bookmarks:index") + "?q=foo&sort=title_asc"
)
def test_save_search_preferences(self): def test_save_search_preferences(self):
user_profile = self.user.profile user_profile = self.user.profile
# no params # no params
self.client.post(reverse('bookmarks:index'), { self.client.post(
'save': '', reverse("bookmarks:index"),
}) {
"save": "",
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_ADDED_DESC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
# with param # with param
self.client.post(reverse('bookmarks:index'), { self.client.post(
'save': '', reverse("bookmarks:index"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
}) "save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
# add a param # add a param
self.client.post(reverse('bookmarks:index'), { self.client.post(
'save': '', reverse("bookmarks:index"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "save": "",
}) "sort": BookmarkSearch.SORT_TITLE_ASC,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
# remove a param # remove a param
self.client.post(reverse('bookmarks:index'), { self.client.post(
'save': '', reverse("bookmarks:index"),
'unread': BookmarkSearch.FILTER_UNREAD_YES, {
}) "save": "",
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_ADDED_DESC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
# ignores non-preferences # ignores non-preferences
self.client.post(reverse('bookmarks:index'), { self.client.post(
'save': '', reverse("bookmarks:index"),
'q': 'foo', {
'user': 'john', "save": "",
'page': '3', "q": "foo",
'sort': BookmarkSearch.SORT_TITLE_ASC, "user": "john",
}) "page": "3",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
def test_url_encode_bookmark_actions_url(self): def test_url_encode_bookmark_actions_url(self):
url = reverse('bookmarks:index') + '?q=%23foo' url = reverse("bookmarks:index") + "?q=%23foo"
response = self.client.get(url) response = self.client.get(url)
html = response.content.decode() html = response.content.decode()
soup = self.make_soup(html) soup = self.make_soup(html)
actions_form = soup.select('form.bookmark-actions')[0] actions_form = soup.select("form.bookmark-actions")[0]
self.assertEqual(actions_form.attrs['action'], self.assertEqual(
'/bookmarks/action?q=%23foo&return_url=%2Fbookmarks%3Fq%3D%2523foo') actions_form.attrs["action"],
"/bookmarks/action?q=%23foo&return_url=%2Fbookmarks%3Fq%3D%2523foo",
)
def test_encode_search_params(self): def test_encode_search_params(self):
bookmark = self.setup_bookmark(description='alert(\'xss\')') bookmark = self.setup_bookmark(description="alert('xss')")
url = reverse('bookmarks:index') + '?q=alert(%27xss%27)' url = reverse("bookmarks:index") + "?q=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
self.assertContains(response, bookmark.url) self.assertContains(response, bookmark.url)
url = reverse('bookmarks:index') + '?sort=alert(%27xss%27)' url = reverse("bookmarks:index") + "?sort=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:index') + '?unread=alert(%27xss%27)' url = reverse("bookmarks:index") + "?unread=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:index') + '?shared=alert(%27xss%27)' url = reverse("bookmarks:index") + "?shared=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:index') + '?user=alert(%27xss%27)' url = reverse("bookmarks:index") + "?user=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:index') + '?page=alert(%27xss%27)' url = reverse("bookmarks:index") + "?page=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")

View file

@ -26,8 +26,10 @@ class BookmarkIndexViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryM
# capture number of queries # capture number of queries
context = CaptureQueriesContext(self.get_connection()) context = CaptureQueriesContext(self.get_connection())
with context: with context:
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks) self.assertContains(
response, "<li ld-bookmark-item>", num_initial_bookmarks
)
number_of_queries = context.final_queries number_of_queries = context.final_queries
@ -38,5 +40,9 @@ class BookmarkIndexViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryM
# assert num queries doesn't increase # assert num queries doesn't increase
with self.assertNumQueries(number_of_queries): with self.assertNumQueries(number_of_queries):
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
self.assertContains(response, '<li ld-bookmark-item>', num_initial_bookmarks + num_additional_bookmarks) self.assertContains(
response,
"<li ld-bookmark-item>",
num_initial_bookmarks + num_additional_bookmarks,
)

View file

@ -15,41 +15,41 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
if overrides is None: if overrides is None:
overrides = {} overrides = {}
form_data = { form_data = {
'url': 'http://example.com', "url": "http://example.com",
'tag_string': 'tag1 tag2', "tag_string": "tag1 tag2",
'title': 'test title', "title": "test title",
'description': 'test description', "description": "test description",
'notes': 'test notes', "notes": "test notes",
'unread': False, "unread": False,
'shared': False, "shared": False,
'auto_close': '', "auto_close": "",
} }
return {**form_data, **overrides} return {**form_data, **overrides}
def test_should_create_new_bookmark(self): def test_should_create_new_bookmark(self):
form_data = self.create_form_data() form_data = self.create_form_data()
self.client.post(reverse('bookmarks:new'), form_data) self.client.post(reverse("bookmarks:new"), form_data)
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
bookmark = Bookmark.objects.first() bookmark = Bookmark.objects.first()
self.assertEqual(bookmark.owner, self.user) self.assertEqual(bookmark.owner, self.user)
self.assertEqual(bookmark.url, form_data['url']) self.assertEqual(bookmark.url, form_data["url"])
self.assertEqual(bookmark.title, form_data['title']) self.assertEqual(bookmark.title, form_data["title"])
self.assertEqual(bookmark.description, form_data['description']) self.assertEqual(bookmark.description, form_data["description"])
self.assertEqual(bookmark.notes, form_data['notes']) self.assertEqual(bookmark.notes, form_data["notes"])
self.assertEqual(bookmark.unread, form_data['unread']) self.assertEqual(bookmark.unread, form_data["unread"])
self.assertEqual(bookmark.shared, form_data['shared']) self.assertEqual(bookmark.shared, form_data["shared"])
self.assertEqual(bookmark.tags.count(), 2) self.assertEqual(bookmark.tags.count(), 2)
tags = bookmark.tags.order_by('name').all() tags = bookmark.tags.order_by("name").all()
self.assertEqual(tags[0].name, 'tag1') self.assertEqual(tags[0].name, "tag1")
self.assertEqual(tags[1].name, 'tag2') self.assertEqual(tags[1].name, "tag2")
def test_should_create_new_unread_bookmark(self): def test_should_create_new_unread_bookmark(self):
form_data = self.create_form_data({'unread': True}) form_data = self.create_form_data({"unread": True})
self.client.post(reverse('bookmarks:new'), form_data) self.client.post(reverse("bookmarks:new"), form_data)
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
@ -57,9 +57,9 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(bookmark.unread) self.assertTrue(bookmark.unread)
def test_should_create_new_shared_bookmark(self): def test_should_create_new_shared_bookmark(self):
form_data = self.create_form_data({'shared': True}) form_data = self.create_form_data({"shared": True})
self.client.post(reverse('bookmarks:new'), form_data) self.client.post(reverse("bookmarks:new"), form_data)
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
@ -67,124 +67,146 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(bookmark.shared) self.assertTrue(bookmark.shared)
def test_should_prefill_url_from_url_parameter(self): 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("bookmarks:new") + "?url=http://example.com")
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
'<input type="text" name="url" value="http://example.com" ' '<input type="text" name="url" value="http://example.com" '
'placeholder=" " autofocus class="form-input" required ' 'placeholder=" " autofocus class="form-input" required '
'id="id_url">', 'id="id_url">',
html) html,
)
def test_should_prefill_title_from_url_parameter(self): def test_should_prefill_title_from_url_parameter(self):
response = self.client.get(reverse('bookmarks:new') + '?title=Example%20Title') response = self.client.get(reverse("bookmarks:new") + "?title=Example%20Title")
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
'<input type="text" name="title" value="Example Title" ' '<input type="text" name="title" value="Example Title" '
'class="form-input" maxlength="512" autocomplete="off" ' 'class="form-input" maxlength="512" autocomplete="off" '
'id="id_title">', 'id="id_title">',
html) html,
)
def test_should_prefill_description_from_url_parameter(self): def test_should_prefill_description_from_url_parameter(self):
response = self.client.get(reverse('bookmarks:new') + '?description=Example%20Site%20Description') response = self.client.get(
reverse("bookmarks:new") + "?description=Example%20Site%20Description"
)
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
'<textarea name="description" class="form-input" cols="40" ' '<textarea name="description" class="form-input" cols="40" '
'rows="2" id="id_description">Example Site Description</textarea>', 'rows="2" id="id_description">Example Site Description</textarea>',
html) html,
)
def test_should_enable_auto_close_when_specified_in_url_parameter(self): def test_should_enable_auto_close_when_specified_in_url_parameter(self):
response = self.client.get( response = self.client.get(reverse("bookmarks:new") + "?auto_close")
reverse('bookmarks:new') + '?auto_close')
html = response.content.decode() html = response.content.decode()
self.assertInHTML( self.assertInHTML(
'<input type="hidden" name="auto_close" value="true" ' '<input type="hidden" name="auto_close" value="true" '
'id="id_auto_close">', 'id="id_auto_close">',
html) html,
)
def test_should_not_enable_auto_close_when_not_specified_in_url_parameter( def test_should_not_enable_auto_close_when_not_specified_in_url_parameter(self):
self): response = self.client.get(reverse("bookmarks:new"))
response = self.client.get(reverse('bookmarks:new'))
html = response.content.decode() html = response.content.decode()
self.assertInHTML('<input type="hidden" name="auto_close" id="id_auto_close">', html) self.assertInHTML(
'<input type="hidden" name="auto_close" id="id_auto_close">', html
)
def test_should_redirect_to_index_view(self): def test_should_redirect_to_index_view(self):
form_data = self.create_form_data() form_data = self.create_form_data()
response = self.client.post(reverse('bookmarks:new'), form_data) response = self.client.post(reverse("bookmarks:new"), form_data)
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
def test_should_not_redirect_to_external_url(self): def test_should_not_redirect_to_external_url(self):
form_data = self.create_form_data() form_data = self.create_form_data()
response = self.client.post(reverse('bookmarks:new') + '?return_url=https://example.com', form_data) response = self.client.post(
reverse("bookmarks:new") + "?return_url=https://example.com", form_data
)
self.assertRedirects(response, reverse('bookmarks:index')) self.assertRedirects(response, reverse("bookmarks:index"))
def test_auto_close_should_redirect_to_close_view(self): 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("bookmarks:new"), form_data)
self.assertRedirects(response, reverse('bookmarks:close')) self.assertRedirects(response, reverse("bookmarks:close"))
def test_should_respect_share_profile_setting(self): def test_should_respect_share_profile_setting(self):
self.user.profile.enable_sharing = False self.user.profile.enable_sharing = False
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse('bookmarks:new')) response = self.client.get(reverse("bookmarks:new"))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(''' self.assertInHTML(
"""
<label for="id_shared" class="form-checkbox"> <label for="id_shared" class="form-checkbox">
<input type="checkbox" name="shared" id="id_shared"> <input type="checkbox" name="shared" id="id_shared">
<i class="form-icon"></i> <i class="form-icon"></i>
<span>Share</span> <span>Share</span>
</label> </label>
''', html, count=0) """,
html,
count=0,
)
self.user.profile.enable_sharing = True self.user.profile.enable_sharing = True
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse('bookmarks:new')) response = self.client.get(reverse("bookmarks:new"))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(''' self.assertInHTML(
"""
<label for="id_shared" class="form-checkbox"> <label for="id_shared" class="form-checkbox">
<input type="checkbox" name="shared" id="id_shared"> <input type="checkbox" name="shared" id="id_shared">
<i class="form-icon"></i> <i class="form-icon"></i>
<span>Share</span> <span>Share</span>
</label> </label>
''', html, count=1) """,
html,
count=1,
)
def test_should_show_respective_share_hint(self): def test_should_show_respective_share_hint(self):
self.user.profile.enable_sharing = True self.user.profile.enable_sharing = True
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse('bookmarks:new')) response = self.client.get(reverse("bookmarks:new"))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(''' self.assertInHTML(
"""
<div class="form-input-hint"> <div class="form-input-hint">
Share this bookmark with other registered users. Share this bookmark with other registered users.
</div> </div>
''', html) """,
html,
)
self.user.profile.enable_public_sharing = True self.user.profile.enable_public_sharing = True
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse('bookmarks:new')) response = self.client.get(reverse("bookmarks:new"))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(''' self.assertInHTML(
"""
<div class="form-input-hint"> <div class="form-input-hint">
Share this bookmark with other registered users and anonymous users. Share this bookmark with other registered users and anonymous users.
</div> </div>
''', html) """,
html,
)
def test_should_hide_notes_if_there_are_no_notes(self): def test_should_hide_notes_if_there_are_no_notes(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id])) response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
self.assertContains(response, '<details class="notes">', count=1) self.assertContains(response, '<details class="notes">', count=1)

View file

@ -9,40 +9,45 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
# no params # no params
search = BookmarkSearch() search = BookmarkSearch()
form = BookmarkSearchForm(search) form = BookmarkSearchForm(search)
self.assertEqual(form['q'].initial, '') self.assertEqual(form["q"].initial, "")
self.assertEqual(form['user'].initial, '') self.assertEqual(form["user"].initial, "")
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_DESC) self.assertEqual(form["sort"].initial, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_OFF) self.assertEqual(form["shared"].initial, BookmarkSearch.FILTER_SHARED_OFF)
self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_OFF) self.assertEqual(form["unread"].initial, BookmarkSearch.FILTER_UNREAD_OFF)
# with params # with params
search = BookmarkSearch(q='search query', search = BookmarkSearch(
q="search query",
sort=BookmarkSearch.SORT_ADDED_ASC, sort=BookmarkSearch.SORT_ADDED_ASC,
user='user123', user="user123",
shared=BookmarkSearch.FILTER_SHARED_SHARED, shared=BookmarkSearch.FILTER_SHARED_SHARED,
unread=BookmarkSearch.FILTER_UNREAD_YES) unread=BookmarkSearch.FILTER_UNREAD_YES,
)
form = BookmarkSearchForm(search) form = BookmarkSearchForm(search)
self.assertEqual(form['q'].initial, 'search query') self.assertEqual(form["q"].initial, "search query")
self.assertEqual(form['user'].initial, 'user123') self.assertEqual(form["user"].initial, "user123")
self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_ASC) self.assertEqual(form["sort"].initial, BookmarkSearch.SORT_ADDED_ASC)
self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_SHARED) self.assertEqual(form["shared"].initial, BookmarkSearch.FILTER_SHARED_SHARED)
self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_YES) self.assertEqual(form["unread"].initial, BookmarkSearch.FILTER_UNREAD_YES)
def test_user_options(self): def test_user_options(self):
users = [ users = [
self.setup_user('user1'), self.setup_user("user1"),
self.setup_user('user2'), self.setup_user("user2"),
self.setup_user('user3'), self.setup_user("user3"),
] ]
search = BookmarkSearch() search = BookmarkSearch()
form = BookmarkSearchForm(search, users=users) form = BookmarkSearchForm(search, users=users)
self.assertCountEqual(form['user'].field.choices, [ self.assertCountEqual(
('', 'Everyone'), form["user"].field.choices,
('user1', 'user1'), [
('user2', 'user2'), ("", "Everyone"),
('user3', 'user3'), ("user1", "user1"),
]) ("user2", "user2"),
("user3", "user3"),
],
)
def test_hidden_fields(self): def test_hidden_fields(self):
# no modified params # no modified params
@ -51,24 +56,27 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
self.assertEqual(len(form.hidden_fields()), 0) self.assertEqual(len(form.hidden_fields()), 0)
# some modified params # some modified params
search = BookmarkSearch(q='search query', search = BookmarkSearch(q="search query", sort=BookmarkSearch.SORT_ADDED_ASC)
sort=BookmarkSearch.SORT_ADDED_ASC)
form = BookmarkSearchForm(search) form = BookmarkSearchForm(search)
self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort']]) self.assertCountEqual(form.hidden_fields(), [form["q"], form["sort"]])
# all modified params # all modified params
search = BookmarkSearch(q='search query', search = BookmarkSearch(
q="search query",
sort=BookmarkSearch.SORT_ADDED_ASC, sort=BookmarkSearch.SORT_ADDED_ASC,
user='user123', user="user123",
shared=BookmarkSearch.FILTER_SHARED_SHARED, shared=BookmarkSearch.FILTER_SHARED_SHARED,
unread=BookmarkSearch.FILTER_UNREAD_YES) unread=BookmarkSearch.FILTER_UNREAD_YES,
)
form = BookmarkSearchForm(search) form = BookmarkSearchForm(search)
self.assertCountEqual(form.hidden_fields(), self.assertCountEqual(
[form['q'], form['sort'], form['user'], form['shared'], form['unread']]) form.hidden_fields(),
[form["q"], form["sort"], form["user"], form["shared"], form["unread"]],
)
# some modified params are editable fields # some modified params are editable fields
search = BookmarkSearch(q='search query', search = BookmarkSearch(
sort=BookmarkSearch.SORT_ADDED_ASC, q="search query", sort=BookmarkSearch.SORT_ADDED_ASC, user="user123"
user='user123') )
form = BookmarkSearchForm(search, editable_fields=['q', 'user']) form = BookmarkSearchForm(search, editable_fields=["q", "user"])
self.assertCountEqual(form.hidden_fields(), [form['sort']]) self.assertCountEqual(form.hidden_fields(), [form["sort"]])

View file

@ -10,57 +10,59 @@ class BookmarkSearchModelTest(TestCase):
query_dict = QueryDict() query_dict = QueryDict()
search = BookmarkSearch.from_request(query_dict) search = BookmarkSearch.from_request(query_dict)
self.assertEqual(search.q, '') self.assertEqual(search.q, "")
self.assertEqual(search.user, '') self.assertEqual(search.user, "")
self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC) self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF) self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF) self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
# some params # some params
query_dict = QueryDict('q=search query&user=user123') query_dict = QueryDict("q=search query&user=user123")
bookmark_search = BookmarkSearch.from_request(query_dict) bookmark_search = BookmarkSearch.from_request(query_dict)
self.assertEqual(bookmark_search.q, 'search query') self.assertEqual(bookmark_search.q, "search query")
self.assertEqual(bookmark_search.user, 'user123') self.assertEqual(bookmark_search.user, "user123")
self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC) self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF) self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF) self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
# all params # all params
query_dict = QueryDict('q=search query&sort=title_asc&user=user123&shared=yes&unread=yes') query_dict = QueryDict(
"q=search query&sort=title_asc&user=user123&shared=yes&unread=yes"
)
search = BookmarkSearch.from_request(query_dict) search = BookmarkSearch.from_request(query_dict)
self.assertEqual(search.q, 'search query') self.assertEqual(search.q, "search query")
self.assertEqual(search.user, 'user123') self.assertEqual(search.user, "user123")
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC) self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED) self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED)
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES) self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
# respects preferences # respects preferences
preferences = { preferences = {
'sort': BookmarkSearch.SORT_TITLE_ASC, "sort": BookmarkSearch.SORT_TITLE_ASC,
'unread': BookmarkSearch.FILTER_UNREAD_YES, "unread": BookmarkSearch.FILTER_UNREAD_YES,
} }
query_dict = QueryDict('q=search query') query_dict = QueryDict("q=search query")
search = BookmarkSearch.from_request(query_dict, preferences) search = BookmarkSearch.from_request(query_dict, preferences)
self.assertEqual(search.q, 'search query') self.assertEqual(search.q, "search query")
self.assertEqual(search.user, '') self.assertEqual(search.user, "")
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC) self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF) self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES) self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
# query overrides preferences # query overrides preferences
preferences = { preferences = {
'sort': BookmarkSearch.SORT_TITLE_ASC, "sort": BookmarkSearch.SORT_TITLE_ASC,
'shared': BookmarkSearch.FILTER_SHARED_SHARED, "shared": BookmarkSearch.FILTER_SHARED_SHARED,
'unread': BookmarkSearch.FILTER_UNREAD_YES, "unread": BookmarkSearch.FILTER_UNREAD_YES,
} }
query_dict = QueryDict('sort=title_desc&shared=no&unread=off') query_dict = QueryDict("sort=title_desc&shared=no&unread=off")
search = BookmarkSearch.from_request(query_dict, preferences) search = BookmarkSearch.from_request(query_dict, preferences)
self.assertEqual(search.q, '') self.assertEqual(search.q, "")
self.assertEqual(search.user, '') self.assertEqual(search.user, "")
self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_DESC) self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_DESC)
self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_UNSHARED) self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_UNSHARED)
self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF) self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
@ -72,28 +74,36 @@ class BookmarkSearchModelTest(TestCase):
self.assertEqual(len(modified_params), 0) self.assertEqual(len(modified_params), 0)
# params are default values # params are default values
bookmark_search = BookmarkSearch(q='', sort=BookmarkSearch.SORT_ADDED_DESC, user='', shared='') bookmark_search = BookmarkSearch(
q="", sort=BookmarkSearch.SORT_ADDED_DESC, user="", shared=""
)
modified_params = bookmark_search.modified_params modified_params = bookmark_search.modified_params
self.assertEqual(len(modified_params), 0) self.assertEqual(len(modified_params), 0)
# some modified params # some modified params
bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC) bookmark_search = BookmarkSearch(
q="search query", sort=BookmarkSearch.SORT_ADDED_ASC
)
modified_params = bookmark_search.modified_params modified_params = bookmark_search.modified_params
self.assertCountEqual(modified_params, ['q', 'sort']) self.assertCountEqual(modified_params, ["q", "sort"])
# all modified params # all modified params
bookmark_search = BookmarkSearch(q='search query', bookmark_search = BookmarkSearch(
q="search query",
sort=BookmarkSearch.SORT_ADDED_ASC, sort=BookmarkSearch.SORT_ADDED_ASC,
user='user123', user="user123",
shared=BookmarkSearch.FILTER_SHARED_SHARED, shared=BookmarkSearch.FILTER_SHARED_SHARED,
unread=BookmarkSearch.FILTER_UNREAD_YES) unread=BookmarkSearch.FILTER_UNREAD_YES,
)
modified_params = bookmark_search.modified_params modified_params = bookmark_search.modified_params
self.assertCountEqual(modified_params, ['q', 'sort', 'user', 'shared', 'unread']) self.assertCountEqual(
modified_params, ["q", "sort", "user", "shared", "unread"]
)
# preferences are not modified params # preferences are not modified params
preferences = { preferences = {
'sort': BookmarkSearch.SORT_TITLE_ASC, "sort": BookmarkSearch.SORT_TITLE_ASC,
'unread': BookmarkSearch.FILTER_UNREAD_YES, "unread": BookmarkSearch.FILTER_UNREAD_YES,
} }
bookmark_search = BookmarkSearch(preferences=preferences) bookmark_search = BookmarkSearch(preferences=preferences)
modified_params = bookmark_search.modified_params modified_params = bookmark_search.modified_params
@ -101,27 +111,31 @@ class BookmarkSearchModelTest(TestCase):
# param is not modified if it matches the preference # param is not modified if it matches the preference
preferences = { preferences = {
'sort': BookmarkSearch.SORT_TITLE_ASC, "sort": BookmarkSearch.SORT_TITLE_ASC,
'unread': BookmarkSearch.FILTER_UNREAD_YES, "unread": BookmarkSearch.FILTER_UNREAD_YES,
} }
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_ASC, bookmark_search = BookmarkSearch(
sort=BookmarkSearch.SORT_TITLE_ASC,
unread=BookmarkSearch.FILTER_UNREAD_YES, unread=BookmarkSearch.FILTER_UNREAD_YES,
preferences=preferences) preferences=preferences,
)
modified_params = bookmark_search.modified_params modified_params = bookmark_search.modified_params
self.assertEqual(len(modified_params), 0) self.assertEqual(len(modified_params), 0)
# overriding preferences is a modified param # overriding preferences is a modified param
preferences = { preferences = {
'sort': BookmarkSearch.SORT_TITLE_ASC, "sort": BookmarkSearch.SORT_TITLE_ASC,
'shared': BookmarkSearch.FILTER_SHARED_SHARED, "shared": BookmarkSearch.FILTER_SHARED_SHARED,
'unread': BookmarkSearch.FILTER_UNREAD_YES, "unread": BookmarkSearch.FILTER_UNREAD_YES,
} }
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC, bookmark_search = BookmarkSearch(
sort=BookmarkSearch.SORT_TITLE_DESC,
shared=BookmarkSearch.FILTER_SHARED_UNSHARED, shared=BookmarkSearch.FILTER_SHARED_UNSHARED,
unread=BookmarkSearch.FILTER_UNREAD_OFF, unread=BookmarkSearch.FILTER_UNREAD_OFF,
preferences=preferences) preferences=preferences,
)
modified_params = bookmark_search.modified_params modified_params = bookmark_search.modified_params
self.assertCountEqual(modified_params, ['sort', 'shared', 'unread']) self.assertCountEqual(modified_params, ["sort", "shared", "unread"])
def test_has_modifications(self): def test_has_modifications(self):
# no params # no params
@ -129,34 +143,49 @@ class BookmarkSearchModelTest(TestCase):
self.assertFalse(bookmark_search.has_modifications) self.assertFalse(bookmark_search.has_modifications)
# params are default values # params are default values
bookmark_search = BookmarkSearch(q='', sort=BookmarkSearch.SORT_ADDED_DESC, user='', shared='') bookmark_search = BookmarkSearch(
q="", sort=BookmarkSearch.SORT_ADDED_DESC, user="", shared=""
)
self.assertFalse(bookmark_search.has_modifications) self.assertFalse(bookmark_search.has_modifications)
# modified params # modified params
bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC) bookmark_search = BookmarkSearch(
q="search query", sort=BookmarkSearch.SORT_ADDED_ASC
)
self.assertTrue(bookmark_search.has_modifications) self.assertTrue(bookmark_search.has_modifications)
def test_preferences_dict(self): def test_preferences_dict(self):
# no params # no params
bookmark_search = BookmarkSearch() bookmark_search = BookmarkSearch()
self.assertEqual(bookmark_search.preferences_dict, { self.assertEqual(
'sort': BookmarkSearch.SORT_ADDED_DESC, bookmark_search.preferences_dict,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
# with params # with params
bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC, unread=BookmarkSearch.FILTER_UNREAD_YES) bookmark_search = BookmarkSearch(
self.assertEqual(bookmark_search.preferences_dict, { sort=BookmarkSearch.SORT_TITLE_DESC, unread=BookmarkSearch.FILTER_UNREAD_YES
'sort': BookmarkSearch.SORT_TITLE_DESC, )
'shared': BookmarkSearch.FILTER_SHARED_OFF, self.assertEqual(
'unread': BookmarkSearch.FILTER_UNREAD_YES, bookmark_search.preferences_dict,
}) {
"sort": BookmarkSearch.SORT_TITLE_DESC,
"shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
# only returns preferences # only returns preferences
bookmark_search = BookmarkSearch(q='search query', user='user123') bookmark_search = BookmarkSearch(q="search query", user="user123")
self.assertEqual(bookmark_search.preferences_dict, { self.assertEqual(
'sort': BookmarkSearch.SORT_ADDED_DESC, bookmark_search.preferences_dict,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)

View file

@ -8,21 +8,25 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all(), mode: str = ''): def render_template(
self, url: str, tags: QuerySet[Tag] = Tag.objects.all(), mode: str = ""
):
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) request = rf.get(url)
request.user = self.get_or_create_test_user() request.user = self.get_or_create_test_user()
request.user_profile = self.get_or_create_test_user().profile request.user_profile = self.get_or_create_test_user().profile
search = BookmarkSearch.from_request(request.GET) search = BookmarkSearch.from_request(request.GET)
context = RequestContext(request, { context = RequestContext(
'request': request, request,
'search': search, {
'tags': tags, "request": request,
'mode': mode, "search": search,
}) "tags": tags,
"mode": mode,
},
)
template_to_render = Template( template_to_render = Template(
'{% load bookmarks %}' "{% load bookmarks %}" "{% bookmark_search search tags mode %}"
'{% bookmark_search search tags mode %}'
) )
return template_to_render.render(context) return template_to_render.render(context)
@ -31,7 +35,7 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertIsNotNone(input) self.assertIsNotNone(input)
if value is not None: if value is not None:
self.assertEqual(input['value'], value) self.assertEqual(input["value"], value)
def assertNoHiddenInput(self, form: BeautifulSoup, name: str): def assertNoHiddenInput(self, form: BeautifulSoup, name: str):
input = form.select_one(f'input[name="{name}"][type="hidden"]') input = form.select_one(f'input[name="{name}"][type="hidden"]')
@ -42,19 +46,19 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertIsNotNone(input) self.assertIsNotNone(input)
if value is not None: if value is not None:
self.assertEqual(input['value'], value) self.assertEqual(input["value"], value)
def assertSelect(self, form: BeautifulSoup, name: str, value: str = None): def assertSelect(self, form: BeautifulSoup, name: str, value: str = None):
select = form.select_one(f'select[name="{name}"]') select = form.select_one(f'select[name="{name}"]')
self.assertIsNotNone(select) self.assertIsNotNone(select)
if value is not None: if value is not None:
options = select.select('option') options = select.select("option")
for option in options: for option in options:
if option['value'] == value: if option["value"] == value:
self.assertTrue(option.has_attr('selected')) self.assertTrue(option.has_attr("selected"))
else: else:
self.assertFalse(option.has_attr('selected')) self.assertFalse(option.has_attr("selected"))
def assertRadioGroup(self, form: BeautifulSoup, name: str, value: str = None): def assertRadioGroup(self, form: BeautifulSoup, name: str, value: str = None):
radios = form.select(f'input[name="{name}"][type="radio"]') radios = form.select(f'input[name="{name}"][type="radio"]')
@ -62,165 +66,182 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
if value is not None: if value is not None:
for radio in radios: for radio in radios:
if radio['value'] == value: if radio["value"] == value:
self.assertTrue(radio.has_attr('checked')) self.assertTrue(radio.has_attr("checked"))
else: else:
self.assertFalse(radio.has_attr('checked')) self.assertFalse(radio.has_attr("checked"))
def assertNoRadioGroup(self, form: BeautifulSoup, name: str): def assertNoRadioGroup(self, form: BeautifulSoup, name: str):
radios = form.select(f'input[name="{name}"][type="radio"]') radios = form.select(f'input[name="{name}"][type="radio"]')
self.assertTrue(len(radios) == 0) self.assertTrue(len(radios) == 0)
def assertUnmodifiedLabel(self, html: str, text: str, id: str = ''): def assertUnmodifiedLabel(self, html: str, text: str, id: str = ""):
id_attr = f'for="{id}"' if id else '' id_attr = f'for="{id}"' if id else ""
tag = 'label' if id else 'div' tag = "label" if id else "div"
needle = f'<{tag} class="form-label" {id_attr}>{text}</{tag}>' needle = f'<{tag} class="form-label" {id_attr}>{text}</{tag}>'
self.assertInHTML(needle, html) self.assertInHTML(needle, html)
def assertModifiedLabel(self, html: str, text: str, id: str = ''): def assertModifiedLabel(self, html: str, text: str, id: str = ""):
id_attr = f'for="{id}"' if id else '' id_attr = f'for="{id}"' if id else ""
tag = 'label' if id else 'div' tag = "label" if id else "div"
needle = f'<{tag} class="form-label text-bold" {id_attr}>{text}</{tag}>' needle = f'<{tag} class="form-label text-bold" {id_attr}>{text}</{tag}>'
self.assertInHTML(needle, html) self.assertInHTML(needle, html)
def test_search_form_inputs(self): def test_search_form_inputs(self):
# Without params # Without params
url = '/test' url = "/test"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
soup = self.make_soup(rendered_template) soup = self.make_soup(rendered_template)
search_form = soup.select_one('form#search') search_form = soup.select_one("form#search")
self.assertSearchInput(search_form, 'q') self.assertSearchInput(search_form, "q")
self.assertNoHiddenInput(search_form, 'user') self.assertNoHiddenInput(search_form, "user")
self.assertNoHiddenInput(search_form, 'sort') self.assertNoHiddenInput(search_form, "sort")
self.assertNoHiddenInput(search_form, 'shared') self.assertNoHiddenInput(search_form, "shared")
self.assertNoHiddenInput(search_form, 'unread') self.assertNoHiddenInput(search_form, "unread")
# With params # With params
url = '/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes' url = "/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
soup = self.make_soup(rendered_template) soup = self.make_soup(rendered_template)
search_form = soup.select_one('form#search') search_form = soup.select_one("form#search")
self.assertSearchInput(search_form, 'q', 'foo') self.assertSearchInput(search_form, "q", "foo")
self.assertHiddenInput(search_form, 'user', 'john') self.assertHiddenInput(search_form, "user", "john")
self.assertHiddenInput(search_form, 'sort', BookmarkSearch.SORT_TITLE_ASC) self.assertHiddenInput(search_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
self.assertHiddenInput(search_form, 'shared', BookmarkSearch.FILTER_SHARED_SHARED) self.assertHiddenInput(
self.assertHiddenInput(search_form, 'unread', BookmarkSearch.FILTER_UNREAD_YES) search_form, "shared", BookmarkSearch.FILTER_SHARED_SHARED
)
self.assertHiddenInput(search_form, "unread", BookmarkSearch.FILTER_UNREAD_YES)
def test_preferences_form_inputs(self): def test_preferences_form_inputs(self):
# Without params # Without params
url = '/test' url = "/test"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
soup = self.make_soup(rendered_template) soup = self.make_soup(rendered_template)
preferences_form = soup.select_one('form#search_preferences') preferences_form = soup.select_one("form#search_preferences")
self.assertNoHiddenInput(preferences_form, 'q') self.assertNoHiddenInput(preferences_form, "q")
self.assertNoHiddenInput(preferences_form, 'user') self.assertNoHiddenInput(preferences_form, "user")
self.assertNoHiddenInput(preferences_form, 'sort') self.assertNoHiddenInput(preferences_form, "sort")
self.assertNoHiddenInput(preferences_form, 'shared') self.assertNoHiddenInput(preferences_form, "shared")
self.assertNoHiddenInput(preferences_form, 'unread') self.assertNoHiddenInput(preferences_form, "unread")
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_ADDED_DESC) self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_ADDED_DESC)
self.assertRadioGroup(preferences_form, 'shared', BookmarkSearch.FILTER_SHARED_OFF) self.assertRadioGroup(
self.assertRadioGroup(preferences_form, 'unread', BookmarkSearch.FILTER_UNREAD_OFF) preferences_form, "shared", BookmarkSearch.FILTER_SHARED_OFF
)
self.assertRadioGroup(
preferences_form, "unread", BookmarkSearch.FILTER_UNREAD_OFF
)
# With params # With params
url = '/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes' url = "/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
soup = self.make_soup(rendered_template) soup = self.make_soup(rendered_template)
preferences_form = soup.select_one('form#search_preferences') preferences_form = soup.select_one("form#search_preferences")
self.assertHiddenInput(preferences_form, 'q', 'foo') self.assertHiddenInput(preferences_form, "q", "foo")
self.assertHiddenInput(preferences_form, 'user', 'john') self.assertHiddenInput(preferences_form, "user", "john")
self.assertNoHiddenInput(preferences_form, 'sort') self.assertNoHiddenInput(preferences_form, "sort")
self.assertNoHiddenInput(preferences_form, 'shared') self.assertNoHiddenInput(preferences_form, "shared")
self.assertNoHiddenInput(preferences_form, 'unread') self.assertNoHiddenInput(preferences_form, "unread")
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_TITLE_ASC) self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
self.assertRadioGroup(preferences_form, 'shared', BookmarkSearch.FILTER_SHARED_SHARED) self.assertRadioGroup(
self.assertRadioGroup(preferences_form, 'unread', BookmarkSearch.FILTER_UNREAD_YES) preferences_form, "shared", BookmarkSearch.FILTER_SHARED_SHARED
)
self.assertRadioGroup(
preferences_form, "unread", BookmarkSearch.FILTER_UNREAD_YES
)
def test_preferences_form_inputs_shared_mode(self): def test_preferences_form_inputs_shared_mode(self):
# Without params # Without params
url = '/test' url = "/test"
rendered_template = self.render_template(url, mode='shared') rendered_template = self.render_template(url, mode="shared")
soup = self.make_soup(rendered_template) soup = self.make_soup(rendered_template)
preferences_form = soup.select_one('form#search_preferences') preferences_form = soup.select_one("form#search_preferences")
self.assertNoHiddenInput(preferences_form, 'q') self.assertNoHiddenInput(preferences_form, "q")
self.assertNoHiddenInput(preferences_form, 'user') self.assertNoHiddenInput(preferences_form, "user")
self.assertNoHiddenInput(preferences_form, 'sort') self.assertNoHiddenInput(preferences_form, "sort")
self.assertNoHiddenInput(preferences_form, 'shared') self.assertNoHiddenInput(preferences_form, "shared")
self.assertNoHiddenInput(preferences_form, 'unread') self.assertNoHiddenInput(preferences_form, "unread")
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_ADDED_DESC) self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_ADDED_DESC)
self.assertNoRadioGroup(preferences_form, 'shared') self.assertNoRadioGroup(preferences_form, "shared")
self.assertNoRadioGroup(preferences_form, 'unread') self.assertNoRadioGroup(preferences_form, "unread")
# With params # With params
url = '/test?q=foo&user=john&sort=title_asc' url = "/test?q=foo&user=john&sort=title_asc"
rendered_template = self.render_template(url, mode='shared') rendered_template = self.render_template(url, mode="shared")
soup = self.make_soup(rendered_template) soup = self.make_soup(rendered_template)
preferences_form = soup.select_one('form#search_preferences') preferences_form = soup.select_one("form#search_preferences")
self.assertHiddenInput(preferences_form, 'q', 'foo') self.assertHiddenInput(preferences_form, "q", "foo")
self.assertHiddenInput(preferences_form, 'user', 'john') self.assertHiddenInput(preferences_form, "user", "john")
self.assertNoHiddenInput(preferences_form, 'sort') self.assertNoHiddenInput(preferences_form, "sort")
self.assertNoHiddenInput(preferences_form, 'shared') self.assertNoHiddenInput(preferences_form, "shared")
self.assertNoHiddenInput(preferences_form, 'unread') self.assertNoHiddenInput(preferences_form, "unread")
self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_TITLE_ASC) self.assertSelect(preferences_form, "sort", BookmarkSearch.SORT_TITLE_ASC)
self.assertNoRadioGroup(preferences_form, 'shared') self.assertNoRadioGroup(preferences_form, "shared")
self.assertNoRadioGroup(preferences_form, 'unread') self.assertNoRadioGroup(preferences_form, "unread")
def test_modified_indicator(self): def test_modified_indicator(self):
# Without modifications # Without modifications
url = '/test' url = "/test"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
self.assertIn('<button type="button" class="btn dropdown-toggle">', rendered_template) self.assertIn(
'<button type="button" class="btn dropdown-toggle">', rendered_template
)
# With modifications # With modifications
url = '/test?sort=title_asc' url = "/test?sort=title_asc"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
self.assertIn('<button type="button" class="btn dropdown-toggle badge">', rendered_template) self.assertIn(
'<button type="button" class="btn dropdown-toggle badge">',
rendered_template,
)
# Ignores non-preferences modifications # Ignores non-preferences modifications
url = '/test?q=foo&user=john' url = "/test?q=foo&user=john"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
self.assertIn('<button type="button" class="btn dropdown-toggle">', rendered_template) self.assertIn(
'<button type="button" class="btn dropdown-toggle">', rendered_template
)
def test_modified_labels(self): def test_modified_labels(self):
# Without modifications # Without modifications
url = '/test' url = "/test"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort') self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
self.assertUnmodifiedLabel(rendered_template, 'Shared filter') self.assertUnmodifiedLabel(rendered_template, "Shared filter")
self.assertUnmodifiedLabel(rendered_template, 'Unread filter') self.assertUnmodifiedLabel(rendered_template, "Unread filter")
# Modified sort # Modified sort
url = '/test?sort=title_asc' url = "/test?sort=title_asc"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
self.assertModifiedLabel(rendered_template, 'Sort by', 'id_sort') self.assertModifiedLabel(rendered_template, "Sort by", "id_sort")
self.assertUnmodifiedLabel(rendered_template, 'Shared filter') self.assertUnmodifiedLabel(rendered_template, "Shared filter")
self.assertUnmodifiedLabel(rendered_template, 'Unread filter') self.assertUnmodifiedLabel(rendered_template, "Unread filter")
# Modified shared # Modified shared
url = '/test?shared=yes' url = "/test?shared=yes"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort') self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
self.assertModifiedLabel(rendered_template, 'Shared filter') self.assertModifiedLabel(rendered_template, "Shared filter")
self.assertUnmodifiedLabel(rendered_template, 'Unread filter') self.assertUnmodifiedLabel(rendered_template, "Unread filter")
# Modified unread # Modified unread
url = '/test?unread=yes' url = "/test?unread=yes"
rendered_template = self.render_template(url) rendered_template = self.render_template(url)
self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort') self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
self.assertUnmodifiedLabel(rendered_template, 'Shared filter') self.assertUnmodifiedLabel(rendered_template, "Shared filter")
self.assertModifiedLabel(rendered_template, 'Unread filter') self.assertModifiedLabel(rendered_template, "Unread filter")

View file

@ -15,39 +15,50 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)
def assertBookmarkCount(self, html: str, bookmark: Bookmark, count: int, link_target: str = '_blank'): def assertBookmarkCount(
self, html: str, bookmark: Bookmark, count: int, link_target: str = "_blank"
):
self.assertInHTML( self.assertInHTML(
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>', f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
html, count=count html,
count=count,
) )
def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'): def assertVisibleBookmarks(
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]') bookmark_list = soup.select_one(
f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]'
)
self.assertIsNotNone(bookmark_list) self.assertIsNotNone(bookmark_list)
bookmark_items = bookmark_list.select('li[ld-bookmark-item]') bookmark_items = bookmark_list.select("li[ld-bookmark-item]")
self.assertEqual(len(bookmark_items), len(bookmarks)) self.assertEqual(len(bookmark_items), len(bookmarks))
for bookmark in bookmarks: for bookmark in bookmarks:
bookmark_item = bookmark_list.select_one( bookmark_item = bookmark_list.select_one(
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]') f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
)
self.assertIsNotNone(bookmark_item) self.assertIsNotNone(bookmark_item)
def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'): def assertInvisibleBookmarks(
self, response, bookmarks: List[Bookmark], link_target: str = "_blank"
):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
for bookmark in bookmarks: for bookmark in bookmarks:
bookmark_item = soup.select_one( bookmark_item = soup.select_one(
f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]') f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]'
)
self.assertIsNone(bookmark_item) self.assertIsNone(bookmark_item)
def assertVisibleTags(self, response, tags: List[Tag]): def assertVisibleTags(self, response, tags: List[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
tag_cloud = soup.select_one('div.tag-cloud') tag_cloud = soup.select_one("div.tag-cloud")
self.assertIsNotNone(tag_cloud) self.assertIsNotNone(tag_cloud)
tag_items = tag_cloud.select('a[data-is-tag-item]') tag_items = tag_cloud.select("a[data-is-tag-item]")
self.assertEqual(len(tag_items), len(tags)) self.assertEqual(len(tag_items), len(tags))
tag_item_names = [tag_item.text.strip() for tag_item in tag_items] tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
@ -57,7 +68,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def assertInvisibleTags(self, response, tags: List[Tag]): def assertInvisibleTags(self, response, tags: List[Tag]):
soup = self.make_soup(response.content.decode()) soup = self.make_soup(response.content.decode())
tag_items = soup.select('a[data-is-tag-item]') tag_items = soup.select("a[data-is-tag-item]")
tag_item_names = [tag_item.text.strip() for tag_item in tag_items] tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
@ -67,26 +78,31 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def assertVisibleUserOptions(self, response, users: List[User]): def assertVisibleUserOptions(self, response, users: List[User]):
html = response.content.decode() html = response.content.decode()
user_options = [ user_options = ['<option value="" selected="">Everyone</option>']
'<option value="" selected="">Everyone</option>'
]
for user in users: for user in users:
user_options.append(f'<option value="{user.username}">{user.username}</option>') user_options.append(
user_select_html = f''' f'<option value="{user.username}">{user.username}</option>'
)
user_select_html = f"""
<select name="user" class="form-select" required="" id="id_user"> <select name="user" class="form-select" required="" id="id_user">
{''.join(user_options)} {''.join(user_options)}
</select> </select>
''' """
self.assertInHTML(user_select_html, html) self.assertInHTML(user_select_html, html)
def assertEditLink(self, response, url): def assertEditLink(self, response, url):
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<a href="{url}">Edit</a> <a href="{url}">Edit</a>
''', html) """,
html,
)
def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(self): def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(
self,
):
self.authenticate() self.authenticate()
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
@ -105,7 +121,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.setup_bookmark(shared=True, user=user4), self.setup_bookmark(shared=True, user=user4),
] ]
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks) self.assertInvisibleBookmarks(response, invisible_bookmarks)
@ -124,7 +140,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.setup_bookmark(shared=True, user=user3), self.setup_bookmark(shared=True, user=user3),
] ]
url = reverse('bookmarks:shared') + '?user=' + user1.username url = reverse("bookmarks:shared") + "?user=" + user1.username
response = self.client.get(url) response = self.client.get(url)
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
@ -134,10 +150,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.authenticate() self.authenticate()
user = self.setup_user(enable_sharing=True) user = self.setup_user(enable_sharing=True)
visible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user, prefix='foo') visible_bookmarks = self.setup_numbered_bookmarks(
3, shared=True, user=user, prefix="foo"
)
invisible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user) 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("bookmarks:shared") + "?q=foo")
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks) self.assertInvisibleBookmarks(response, invisible_bookmarks)
@ -146,15 +164,21 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True) user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
visible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user1, prefix='user1') visible_bookmarks = self.setup_numbered_bookmarks(
invisible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user2, prefix='user2') 3, shared=True, user=user1, prefix="user1"
)
invisible_bookmarks = self.setup_numbered_bookmarks(
3, shared=True, user=user2, prefix="user2"
)
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_bookmarks) self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_tags_for_shared_bookmarks_from_all_users_that_have_sharing_enabled(self): def test_should_list_tags_for_shared_bookmarks_from_all_users_that_have_sharing_enabled(
self,
):
self.authenticate() self.authenticate()
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
@ -181,7 +205,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.setup_bookmark(shared=False, user=user3, tags=[invisible_tags[2]]) self.setup_bookmark(shared=False, user=user3, tags=[invisible_tags[2]])
self.setup_bookmark(shared=True, user=user4, tags=[invisible_tags[3]]) self.setup_bookmark(shared=True, user=user4, tags=[invisible_tags[3]])
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertVisibleTags(response, visible_tags) self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags) self.assertInvisibleTags(response, invisible_tags)
@ -203,7 +227,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]]) self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[1]]) self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[1]])
url = reverse('bookmarks:shared') + '?user=' + user1.username url = reverse("bookmarks:shared") + "?user=" + user1.username
response = self.client.get(url) response = self.client.get(url)
self.assertVisibleTags(response, visible_tags) self.assertVisibleTags(response, visible_tags)
@ -225,15 +249,21 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.setup_tag(user=user3), self.setup_tag(user=user3),
] ]
self.setup_bookmark(shared=True, user=user1, title='searchvalue', tags=[visible_tags[0]]) self.setup_bookmark(
self.setup_bookmark(shared=True, user=user2, title='searchvalue', tags=[visible_tags[1]]) shared=True, user=user1, title="searchvalue", tags=[visible_tags[0]]
self.setup_bookmark(shared=True, user=user3, title='searchvalue', tags=[visible_tags[2]]) )
self.setup_bookmark(
shared=True, user=user2, title="searchvalue", tags=[visible_tags[1]]
)
self.setup_bookmark(
shared=True, user=user3, title="searchvalue", tags=[visible_tags[2]]
)
self.setup_bookmark(shared=True, user=user1, tags=[invisible_tags[0]]) self.setup_bookmark(shared=True, user=user1, tags=[invisible_tags[0]])
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]]) self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[2]]) 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("bookmarks:shared") + "?q=searchvalue")
self.assertVisibleTags(response, visible_tags) self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags) self.assertInvisibleTags(response, invisible_tags)
@ -257,7 +287,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]]) self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]]) self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertVisibleTags(response, visible_tags) self.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_tags) self.assertInvisibleTags(response, invisible_tags)
@ -265,8 +295,8 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self): def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self):
self.authenticate() self.authenticate()
expected_visible_users = [ expected_visible_users = [
self.setup_user(name='user_a', enable_sharing=True), self.setup_user(name="user_a", enable_sharing=True),
self.setup_user(name='user_b', enable_sharing=True), self.setup_user(name="user_b", enable_sharing=True),
] ]
self.setup_bookmark(shared=True, user=expected_visible_users[0]) self.setup_bookmark(shared=True, user=expected_visible_users[0])
self.setup_bookmark(shared=True, user=expected_visible_users[1]) self.setup_bookmark(shared=True, user=expected_visible_users[1])
@ -274,14 +304,18 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.setup_bookmark(shared=False, user=self.setup_user(enable_sharing=True)) self.setup_bookmark(shared=False, user=self.setup_user(enable_sharing=True))
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=False)) self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=False))
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertVisibleUserOptions(response, expected_visible_users) self.assertVisibleUserOptions(response, expected_visible_users)
def test_should_list_only_users_with_publicly_shared_bookmarks_without_login(self): def test_should_list_only_users_with_publicly_shared_bookmarks_without_login(self):
# users with public sharing enabled # users with public sharing enabled
expected_visible_users = [ expected_visible_users = [
self.setup_user(name='user_a', enable_sharing=True, enable_public_sharing=True), self.setup_user(
self.setup_user(name='user_b', enable_sharing=True, enable_public_sharing=True), name="user_a", enable_sharing=True, enable_public_sharing=True
),
self.setup_user(
name="user_b", enable_sharing=True, enable_public_sharing=True
),
] ]
self.setup_bookmark(shared=True, user=expected_visible_users[0]) self.setup_bookmark(shared=True, user=expected_visible_users[0])
self.setup_bookmark(shared=True, user=expected_visible_users[1]) self.setup_bookmark(shared=True, user=expected_visible_users[1])
@ -290,7 +324,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=True)) self.setup_bookmark(shared=True, user=self.setup_user(enable_sharing=True))
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("bookmarks:shared"))
self.assertVisibleUserOptions(response, expected_visible_users) self.assertVisibleUserOptions(response, expected_visible_users)
def test_should_list_bookmarks_and_tags_for_search_preferences(self): def test_should_list_bookmarks_and_tags_for_search_preferences(self):
@ -299,19 +333,33 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
user_profile = self.get_or_create_test_user().profile user_profile = self.get_or_create_test_user().profile
user_profile.search_preferences = { user_profile.search_preferences = {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "unread": BookmarkSearch.FILTER_UNREAD_YES,
} }
user_profile.save() user_profile.save()
unread_bookmarks = self.setup_numbered_bookmarks(3, shared=True, unread=True, with_tags=True, prefix='unread', unread_bookmarks = self.setup_numbered_bookmarks(
tag_prefix='unread', user=other_user) 3,
read_bookmarks = self.setup_numbered_bookmarks(3, shared=True, unread=False, with_tags=True, prefix='read', shared=True,
tag_prefix='read', user=other_user) unread=True,
with_tags=True,
prefix="unread",
tag_prefix="unread",
user=other_user,
)
read_bookmarks = self.setup_numbered_bookmarks(
3,
shared=True,
unread=False,
with_tags=True,
prefix="read",
tag_prefix="read",
user=other_user,
)
unread_tags = self.get_tags_from_bookmarks(unread_bookmarks) unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
read_tags = self.get_tags_from_bookmarks(read_bookmarks) read_tags = self.get_tags_from_bookmarks(read_bookmarks)
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertVisibleBookmarks(response, unread_bookmarks) self.assertVisibleBookmarks(response, unread_bookmarks)
self.assertInvisibleBookmarks(response, read_bookmarks) self.assertInvisibleBookmarks(response, read_bookmarks)
self.assertVisibleTags(response, unread_tags) self.assertVisibleTags(response, unread_tags)
@ -325,12 +373,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
visible_bookmarks = [ visible_bookmarks = [
self.setup_bookmark(shared=True), self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True), self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True) self.setup_bookmark(shared=True),
] ]
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank') self.assertVisibleBookmarks(response, visible_bookmarks, "_blank")
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self): def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
self.authenticate() self.authenticate()
@ -342,12 +390,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
visible_bookmarks = [ visible_bookmarks = [
self.setup_bookmark(shared=True), self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True), self.setup_bookmark(shared=True),
self.setup_bookmark(shared=True) self.setup_bookmark(shared=True),
] ]
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertVisibleBookmarks(response, visible_bookmarks, '_self') self.assertVisibleBookmarks(response, visible_bookmarks, "_self")
def test_edit_link_return_url_respects_search_options(self): def test_edit_link_return_url_respects_search_options(self):
self.authenticate() self.authenticate()
@ -355,180 +403,227 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
user.profile.enable_sharing = True user.profile.enable_sharing = True
user.profile.save() user.profile.save()
bookmark = self.setup_bookmark(title='foo', shared=True, user=user) bookmark = self.setup_bookmark(title="foo", shared=True, user=user)
edit_url = reverse('bookmarks:edit', args=[bookmark.id]) edit_url = reverse("bookmarks:edit", args=[bookmark.id])
base_url = reverse('bookmarks:shared') base_url = reverse("bookmarks:shared")
# without query params # without query params
return_url = urllib.parse.quote(base_url) return_url = urllib.parse.quote(base_url)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url) response = self.client.get(base_url)
self.assertEditLink(response, url) self.assertEditLink(response, url)
# with query # with query
url_params = '?q=foo' url_params = "?q=foo"
return_url = urllib.parse.quote(base_url + url_params) return_url = urllib.parse.quote(base_url + url_params)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertEditLink(response, url) self.assertEditLink(response, url)
# with query and user # with query and user
url_params = f'?q=foo&user={user.username}' url_params = f"?q=foo&user={user.username}"
return_url = urllib.parse.quote(base_url + url_params) return_url = urllib.parse.quote(base_url + url_params)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertEditLink(response, url) self.assertEditLink(response, url)
# with query and sort and page # with query and sort and page
url_params = '?q=foo&sort=title_asc&page=2' url_params = "?q=foo&sort=title_asc&page=2"
return_url = urllib.parse.quote(base_url + url_params) return_url = urllib.parse.quote(base_url + url_params)
url = f'{edit_url}?return_url={return_url}' url = f"{edit_url}?return_url={return_url}"
response = self.client.get(base_url + url_params) response = self.client.get(base_url + url_params)
self.assertEditLink(response, url) self.assertEditLink(response, url)
def test_apply_search_preferences(self): def test_apply_search_preferences(self):
# no params # no params
response = self.client.post(reverse('bookmarks:shared')) response = self.client.post(reverse("bookmarks:shared"))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:shared')) self.assertEqual(response.url, reverse("bookmarks:shared"))
# some params # some params
response = self.client.post(reverse('bookmarks:shared'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:shared"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
}) "q": "foo",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&sort=title_asc') self.assertEqual(
response.url, reverse("bookmarks:shared") + "?q=foo&sort=title_asc"
)
# params with default value are removed # params with default value are removed
response = self.client.post(reverse('bookmarks:shared'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:shared"),
'user': '', {
'sort': BookmarkSearch.SORT_ADDED_DESC, "q": "foo",
'shared': BookmarkSearch.FILTER_SHARED_OFF, "user": "",
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&unread=yes') self.assertEqual(
response.url, reverse("bookmarks:shared") + "?q=foo&unread=yes"
)
# page is removed # page is removed
response = self.client.post(reverse('bookmarks:shared'), { response = self.client.post(
'q': 'foo', reverse("bookmarks:shared"),
'page': '2', {
'sort': BookmarkSearch.SORT_TITLE_ASC, "q": "foo",
}) "page": "2",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&sort=title_asc') self.assertEqual(
response.url, reverse("bookmarks:shared") + "?q=foo&sort=title_asc"
)
def test_save_search_preferences(self): def test_save_search_preferences(self):
self.authenticate() self.authenticate()
user_profile = self.user.profile user_profile = self.user.profile
# no params # no params
self.client.post(reverse('bookmarks:shared'), { self.client.post(
'save': '', reverse("bookmarks:shared"),
}) {
"save": "",
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_ADDED_DESC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
# with param # with param
self.client.post(reverse('bookmarks:shared'), { self.client.post(
'save': '', reverse("bookmarks:shared"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
}) "save": "",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
# add a param # add a param
self.client.post(reverse('bookmarks:shared'), { self.client.post(
'save': '', reverse("bookmarks:shared"),
'sort': BookmarkSearch.SORT_TITLE_ASC, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "save": "",
}) "sort": BookmarkSearch.SORT_TITLE_ASC,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
# remove a param # remove a param
self.client.post(reverse('bookmarks:shared'), { self.client.post(
'save': '', reverse("bookmarks:shared"),
'unread': BookmarkSearch.FILTER_UNREAD_YES, {
}) "save": "",
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_ADDED_DESC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_YES, "sort": BookmarkSearch.SORT_ADDED_DESC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_YES,
},
)
# ignores non-preferences # ignores non-preferences
self.client.post(reverse('bookmarks:shared'), { self.client.post(
'save': '', reverse("bookmarks:shared"),
'q': 'foo', {
'user': 'john', "save": "",
'page': '3', "q": "foo",
'sort': BookmarkSearch.SORT_TITLE_ASC, "user": "john",
}) "page": "3",
"sort": BookmarkSearch.SORT_TITLE_ASC,
},
)
user_profile.refresh_from_db() user_profile.refresh_from_db()
self.assertEqual(user_profile.search_preferences, { self.assertEqual(
'sort': BookmarkSearch.SORT_TITLE_ASC, user_profile.search_preferences,
'shared': BookmarkSearch.FILTER_SHARED_OFF, {
'unread': BookmarkSearch.FILTER_UNREAD_OFF, "sort": BookmarkSearch.SORT_TITLE_ASC,
}) "shared": BookmarkSearch.FILTER_SHARED_OFF,
"unread": BookmarkSearch.FILTER_UNREAD_OFF,
},
)
def test_url_encode_bookmark_actions_url(self): def test_url_encode_bookmark_actions_url(self):
url = reverse('bookmarks:shared') + '?q=%23foo' url = reverse("bookmarks:shared") + "?q=%23foo"
response = self.client.get(url) response = self.client.get(url)
html = response.content.decode() html = response.content.decode()
soup = self.make_soup(html) soup = self.make_soup(html)
actions_form = soup.select('form.bookmark-actions')[0] actions_form = soup.select("form.bookmark-actions")[0]
self.assertEqual(actions_form.attrs['action'], self.assertEqual(
'/bookmarks/shared/action?q=%23foo&return_url=%2Fbookmarks%2Fshared%3Fq%3D%2523foo') actions_form.attrs["action"],
"/bookmarks/shared/action?q=%23foo&return_url=%2Fbookmarks%2Fshared%3Fq%3D%2523foo",
)
def test_encode_search_params(self): def test_encode_search_params(self):
self.authenticate() self.authenticate()
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
user.profile.enable_sharing = True user.profile.enable_sharing = True
user.profile.save() user.profile.save()
bookmark = self.setup_bookmark(description='alert(\'xss\')', shared=True) bookmark = self.setup_bookmark(description="alert('xss')", shared=True)
url = reverse('bookmarks:shared') + '?q=alert(%27xss%27)' url = reverse("bookmarks:shared") + "?q=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
self.assertContains(response, bookmark.url) self.assertContains(response, bookmark.url)
url = reverse('bookmarks:shared') + '?sort=alert(%27xss%27)' url = reverse("bookmarks:shared") + "?sort=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:shared') + '?unread=alert(%27xss%27)' url = reverse("bookmarks:shared") + "?unread=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:shared') + '?shared=alert(%27xss%27)' url = reverse("bookmarks:shared") + "?shared=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:shared') + '?user=alert(%27xss%27)' url = reverse("bookmarks:shared") + "?user=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")
url = reverse('bookmarks:shared') + '?page=alert(%27xss%27)' url = reverse("bookmarks:shared") + "?page=alert(%27xss%27)"
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, 'alert(\'xss\')') self.assertNotContains(response, "alert('xss')")

View file

@ -27,8 +27,10 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
# capture number of queries # capture number of queries
context = CaptureQueriesContext(self.get_connection()) context = CaptureQueriesContext(self.get_connection())
with context: with context:
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertContains(response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks) self.assertContains(
response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks
)
number_of_queries = context.final_queries number_of_queries = context.final_queries
@ -40,5 +42,9 @@ class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactory
# assert num queries doesn't increase # assert num queries doesn't increase
with self.assertNumQueries(number_of_queries): with self.assertNumQueries(number_of_queries):
response = self.client.get(reverse('bookmarks:shared')) response = self.client.get(reverse("bookmarks:shared"))
self.assertContains(response, '<li ld-bookmark-item class="shared">', num_initial_bookmarks + num_additional_bookmarks) self.assertContains(
response,
'<li ld-bookmark-item class="shared">',
num_initial_bookmarks + num_additional_bookmarks,
)

View file

@ -9,36 +9,38 @@ from bookmarks.models import BookmarkForm, Bookmark
User = get_user_model() User = get_user_model()
ENABLED_URL_VALIDATION_TEST_CASES = [ ENABLED_URL_VALIDATION_TEST_CASES = [
('thisisnotavalidurl', False), ("thisisnotavalidurl", False),
('http://domain', False), ("http://domain", False),
('unknownscheme://domain.com', False), ("unknownscheme://domain.com", False),
('http://domain.com', True), ("http://domain.com", True),
('http://www.domain.com', True), ("http://www.domain.com", True),
('https://domain.com', True), ("https://domain.com", True),
('https://www.domain.com', True), ("https://www.domain.com", True),
] ]
DISABLED_URL_VALIDATION_TEST_CASES = [ DISABLED_URL_VALIDATION_TEST_CASES = [
('thisisnotavalidurl', True), ("thisisnotavalidurl", True),
('http://domain', True), ("http://domain", True),
('unknownscheme://domain.com', True), ("unknownscheme://domain.com", True),
('http://domain.com', True), ("http://domain.com", True),
('http://www.domain.com', True), ("http://www.domain.com", True),
('https://domain.com', True), ("https://domain.com", True),
('https://www.domain.com', True), ("https://www.domain.com", True),
] ]
class BookmarkValidationTestCase(TestCase): class BookmarkValidationTestCase(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.user = User.objects.create_user('testuser', 'test@example.com', 'password123') self.user = User.objects.create_user(
"testuser", "test@example.com", "password123"
)
def test_bookmark_model_should_not_allow_missing_url(self): def test_bookmark_model_should_not_allow_missing_url(self):
bookmark = Bookmark( bookmark = Bookmark(
date_added=datetime.datetime.now(), date_added=datetime.datetime.now(),
date_modified=datetime.datetime.now(), date_modified=datetime.datetime.now(),
owner=self.user owner=self.user,
) )
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
@ -46,10 +48,10 @@ class BookmarkValidationTestCase(TestCase):
def test_bookmark_model_should_not_allow_empty_url(self): def test_bookmark_model_should_not_allow_empty_url(self):
bookmark = Bookmark( bookmark = Bookmark(
url='', url="",
date_added=datetime.datetime.now(), date_added=datetime.datetime.now(),
date_modified=datetime.datetime.now(), date_modified=datetime.datetime.now(),
owner=self.user owner=self.user,
) )
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
@ -64,15 +66,15 @@ class BookmarkValidationTestCase(TestCase):
self._run_bookmark_model_url_validity_checks(DISABLED_URL_VALIDATION_TEST_CASES) self._run_bookmark_model_url_validity_checks(DISABLED_URL_VALIDATION_TEST_CASES)
def test_bookmark_form_should_validate_required_fields(self): def test_bookmark_form_should_validate_required_fields(self):
form = BookmarkForm(data={'url': ''}) form = BookmarkForm(data={"url": ""})
self.assertEqual(len(form.errors), 1) self.assertEqual(len(form.errors), 1)
self.assertIn('required', str(form.errors)) self.assertIn("required", str(form.errors))
form = BookmarkForm(data={'url': None}) form = BookmarkForm(data={"url": None})
self.assertEqual(len(form.errors), 1) self.assertEqual(len(form.errors), 1)
self.assertIn('required', str(form.errors)) self.assertIn("required", str(form.errors))
@override_settings(LD_DISABLE_URL_VALIDATION=False) @override_settings(LD_DISABLE_URL_VALIDATION=False)
def test_bookmark_form_should_validate_url_if_not_disabled_in_settings(self): def test_bookmark_form_should_validate_url_if_not_disabled_in_settings(self):
@ -89,23 +91,25 @@ class BookmarkValidationTestCase(TestCase):
url=url, url=url,
date_added=datetime.datetime.now(), date_added=datetime.datetime.now(),
date_modified=datetime.datetime.now(), date_modified=datetime.datetime.now(),
owner=self.user owner=self.user,
) )
try: try:
bookmark.full_clean() bookmark.full_clean()
self.assertTrue(expectation, 'Did not expect validation error') self.assertTrue(expectation, "Did not expect validation error")
except ValidationError as e: except ValidationError as e:
self.assertFalse(expectation, 'Expected validation error') self.assertFalse(expectation, "Expected validation error")
self.assertTrue('url' in e.message_dict, 'Expected URL validation to fail') self.assertTrue(
"url" in e.message_dict, "Expected URL validation to fail"
)
def _run_bookmark_form_url_validity_checks(self, cases): def _run_bookmark_form_url_validity_checks(self, cases):
for case in cases: for case in cases:
url, expectation = case url, expectation = case
form = BookmarkForm(data={'url': url}) form = BookmarkForm(data={"url": url})
if expectation: if expectation:
self.assertEqual(len(form.errors), 0) self.assertEqual(len(form.errors), 0)
else: else:
self.assertEqual(len(form.errors), 1) self.assertEqual(len(form.errors), 1)
self.assertIn('Enter a valid URL', str(form.errors)) self.assertIn("Enter a valid URL", str(form.errors))

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,10 @@ from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def setUp(self) -> None: def setUp(self) -> None:
self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0] self.api_token = Token.objects.get_or_create(
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key) user=self.get_or_create_test_user()
)[0]
self.client.credentials(HTTP_AUTHORIZATION="Token " + self.api_token.key)
def get_connection(self): def get_connection(self):
return connections[DEFAULT_DB_ALIAS] return connections[DEFAULT_DB_ALIAS]
@ -26,7 +28,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
# capture number of queries # capture number of queries
context = CaptureQueriesContext(self.get_connection()) context = CaptureQueriesContext(self.get_connection())
with context: with context:
self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK) self.get(
reverse("bookmarks:bookmark-list"),
expected_status_code=status.HTTP_200_OK,
)
number_of_queries = context.final_queries number_of_queries = context.final_queries
@ -41,7 +46,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
# capture number of queries # capture number of queries
context = CaptureQueriesContext(self.get_connection()) context = CaptureQueriesContext(self.get_connection())
with context: with context:
self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK) self.get(
reverse("bookmarks:bookmark-archived"),
expected_status_code=status.HTTP_200_OK,
)
number_of_queries = context.final_queries number_of_queries = context.final_queries
@ -57,7 +65,10 @@ class BookmarksApiPerformanceTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
# capture number of queries # capture number of queries
context = CaptureQueriesContext(self.get_connection()) context = CaptureQueriesContext(self.get_connection())
with context: with context:
self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK) self.get(
reverse("bookmarks:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)
number_of_queries = context.final_queries number_of_queries = context.final_queries

View file

@ -9,47 +9,68 @@ from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def authenticate(self) -> None: def authenticate(self) -> None:
self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0] self.api_token = Token.objects.get_or_create(
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key) user=self.get_or_create_test_user()
)[0]
self.client.credentials(HTTP_AUTHORIZATION="Token " + self.api_token.key)
def test_list_bookmarks_requires_authentication(self): def test_list_bookmarks_requires_authentication(self):
self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_401_UNAUTHORIZED) self.get(
reverse("bookmarks:bookmark-list"),
expected_status_code=status.HTTP_401_UNAUTHORIZED,
)
self.authenticate() self.authenticate()
self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK) self.get(
reverse("bookmarks:bookmark-list"), expected_status_code=status.HTTP_200_OK
)
def test_list_archived_bookmarks_requires_authentication(self): def test_list_archived_bookmarks_requires_authentication(self):
self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_401_UNAUTHORIZED) self.get(
reverse("bookmarks:bookmark-archived"),
expected_status_code=status.HTTP_401_UNAUTHORIZED,
)
self.authenticate() self.authenticate()
self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK) self.get(
reverse("bookmarks:bookmark-archived"),
expected_status_code=status.HTTP_200_OK,
)
def test_list_shared_bookmarks_does_not_require_authentication(self): def test_list_shared_bookmarks_does_not_require_authentication(self):
self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK) self.get(
reverse("bookmarks:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)
self.authenticate() self.authenticate()
self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK) self.get(
reverse("bookmarks:bookmark-shared"),
expected_status_code=status.HTTP_200_OK,
)
def test_create_bookmark_requires_authentication(self): def test_create_bookmark_requires_authentication(self):
data = { data = {
'url': 'https://example.com/', "url": "https://example.com/",
'title': 'Test title', "title": "Test title",
'description': 'Test description', "description": "Test description",
'notes': 'Test notes', "notes": "Test notes",
'is_archived': False, "is_archived": False,
'unread': False, "unread": False,
'shared': False, "shared": False,
'tag_names': ['tag1', 'tag2'] "tag_names": ["tag1", "tag2"],
} }
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_401_UNAUTHORIZED) self.post(
reverse("bookmarks:bookmark-list"), data, status.HTTP_401_UNAUTHORIZED
)
self.authenticate() self.authenticate()
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) self.post(reverse("bookmarks:bookmark-list"), data, status.HTTP_201_CREATED)
def test_get_bookmark_requires_authentication(self): def test_get_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED) self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@ -58,8 +79,8 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_update_bookmark_requires_authentication(self): def test_update_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
data = {'url': 'https://example.com/'} data = {"url": "https://example.com/"}
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
self.put(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED) self.put(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@ -68,8 +89,8 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_patch_bookmark_requires_authentication(self): def test_patch_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
data = {'url': 'https://example.com'} data = {"url": "https://example.com"}
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
self.patch(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED) self.patch(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@ -78,7 +99,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_delete_bookmark_requires_authentication(self): def test_delete_bookmark_requires_authentication(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) url = reverse("bookmarks:bookmark-detail", args=[bookmark.id])
self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED) self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@ -87,7 +108,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_archive_requires_authentication(self): def test_archive_requires_authentication(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
url = reverse('bookmarks:bookmark-archive', args=[bookmark.id]) url = reverse("bookmarks:bookmark-archive", args=[bookmark.id])
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED) self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@ -96,7 +117,7 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
def test_unarchive_requires_authentication(self): def test_unarchive_requires_authentication(self):
bookmark = self.setup_bookmark(is_archived=True) bookmark = self.setup_bookmark(is_archived=True)
url = reverse('bookmarks:bookmark-unarchive', args=[bookmark.id]) url = reverse("bookmarks:bookmark-unarchive", args=[bookmark.id])
self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED) self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
@ -104,16 +125,18 @@ class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin)
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT) self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
def test_check_requires_authentication(self): def test_check_requires_authentication(self):
url = reverse('bookmarks:bookmark-check') url = reverse("bookmarks:bookmark-check")
check_url = urllib.parse.quote_plus('https://example.com') check_url = urllib.parse.quote_plus("https://example.com")
self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_401_UNAUTHORIZED) self.get(
f"{url}?url={check_url}", expected_status_code=status.HTTP_401_UNAUTHORIZED
)
self.authenticate() self.authenticate()
self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK) self.get(f"{url}?url={check_url}", expected_status_code=status.HTTP_200_OK)
def test_user_profile_requires_authentication(self): def test_user_profile_requires_authentication(self):
url = reverse('bookmarks:user-profile') url = reverse("bookmarks:user-profile")
self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED) self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)

View file

@ -16,34 +16,48 @@ from bookmarks.views.partials import contexts
class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin): class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank'): def assertBookmarksLink(
favicon_img = f'<img src="/static/{bookmark.favicon_file}" alt="">' if bookmark.favicon_file else '' self, html: str, bookmark: Bookmark, link_target: str = "_blank"
):
favicon_img = (
f'<img src="/static/{bookmark.favicon_file}" alt="">'
if bookmark.favicon_file
else ""
)
self.assertInHTML( self.assertInHTML(
f''' f"""
<a href="{bookmark.url}" <a href="{bookmark.url}"
target="{link_target}" target="{link_target}"
rel="noopener"> rel="noopener">
{favicon_img} {favicon_img}
<span>{bookmark.resolved_title}</span> <span>{bookmark.resolved_title}</span>
</a> </a>
''', """,
html html,
) )
def assertDateLabel(self, html: str, label_content: str): def assertDateLabel(self, html: str, label_content: str):
self.assertInHTML(f''' self.assertInHTML(
f"""
<span>{label_content}</span> <span>{label_content}</span>
<span class="separator">|</span> <span class="separator">|</span>
''', html) """,
html,
)
def assertWebArchiveLink(self, html: str, label_content: str, url: str, link_target: str = '_blank'): def assertWebArchiveLink(
self.assertInHTML(f''' self, html: str, label_content: str, url: str, link_target: str = "_blank"
):
self.assertInHTML(
f"""
<a href="{url}" <a href="{url}"
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener"> title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
{label_content} {label_content}
</a> </a>
<span class="separator">|</span> <span class="separator">|</span>
''', html) """,
html,
)
def assertBookmarkActions(self, html: str, bookmark: Bookmark): def assertBookmarkActions(self, html: str, bookmark: Bookmark):
self.assertBookmarkActionsCount(html, bookmark, count=1) self.assertBookmarkActionsCount(html, bookmark, count=1)
@ -53,20 +67,32 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, count=1): def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, count=1):
# Edit link # Edit link
edit_url = reverse('bookmarks:edit', args=[bookmark.id]) edit_url = reverse("bookmarks:edit", args=[bookmark.id])
self.assertInHTML(f''' self.assertInHTML(
f"""
<a href="{edit_url}?return_url=/bookmarks">Edit</a> <a href="{edit_url}?return_url=/bookmarks">Edit</a>
''', html, count=count) """,
html,
count=count,
)
# Archive link # Archive link
self.assertInHTML(f''' self.assertInHTML(
f"""
<button type="submit" name="archive" value="{bookmark.id}" <button type="submit" name="archive" value="{bookmark.id}"
class="btn btn-link btn-sm">Archive</button> class="btn btn-link btn-sm">Archive</button>
''', html, count=count) """,
html,
count=count,
)
# Delete link # Delete link
self.assertInHTML(f''' self.assertInHTML(
f"""
<button ld-confirm-button type="submit" name="remove" value="{bookmark.id}" <button ld-confirm-button type="submit" name="remove" value="{bookmark.id}"
class="btn btn-link btn-sm">Remove</button> class="btn btn-link btn-sm">Remove</button>
''', html, count=count) """,
html,
count=count,
)
def assertShareInfo(self, html: str, bookmark: Bookmark): def assertShareInfo(self, html: str, bookmark: Bookmark):
self.assertShareInfoCount(html, bookmark, 1) self.assertShareInfoCount(html, bookmark, 1)
@ -75,11 +101,15 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.assertShareInfoCount(html, bookmark, 0) self.assertShareInfoCount(html, bookmark, 0)
def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1): def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
self.assertInHTML(f''' self.assertInHTML(
f"""
<span>Shared by <span>Shared by
<a href="?user={bookmark.owner.username}">{bookmark.owner.username}</a> <a href="?user={bookmark.owner.username}">{bookmark.owner.username}</a>
</span> </span>
''', html, count=count) """,
html,
count=count,
)
def assertFaviconVisible(self, html: str, bookmark: Bookmark): def assertFaviconVisible(self, html: str, bookmark: Bookmark):
self.assertFaviconCount(html, bookmark, 1) self.assertFaviconCount(html, bookmark, 1)
@ -88,47 +118,68 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.assertFaviconCount(html, bookmark, 0) self.assertFaviconCount(html, bookmark, 0)
def assertFaviconCount(self, html: str, bookmark: Bookmark, count=1): def assertFaviconCount(self, html: str, bookmark: Bookmark, count=1):
self.assertInHTML(f''' self.assertInHTML(
f"""
<img src="/static/{bookmark.favicon_file}" alt=""> <img src="/static/{bookmark.favicon_file}" alt="">
''', html, count=count) """,
html,
count=count,
)
def assertBookmarkURLCount(self, html: str, bookmark: Bookmark, link_target: str = '_blank', count=0): def assertBookmarkURLCount(
self.assertInHTML(f''' self, html: str, bookmark: Bookmark, link_target: str = "_blank", count=0
):
self.assertInHTML(
f"""
<div class="url-path truncate"> <div class="url-path truncate">
<a href="{bookmark.url}" target="{link_target}" rel="noopener" <a href="{bookmark.url}" target="{link_target}" rel="noopener"
class="url-display text-sm"> class="url-display text-sm">
{bookmark.url} {bookmark.url}
</a> </a>
</div> </div>
''', html, count) """,
html,
count,
)
def assertBookmarkURLVisible(self, html: str, bookmark: Bookmark): def assertBookmarkURLVisible(self, html: str, bookmark: Bookmark):
self.assertBookmarkURLCount(html, bookmark, count=1) self.assertBookmarkURLCount(html, bookmark, count=1)
def assertBookmarkURLHidden(self, html: str, bookmark: Bookmark, link_target: str = '_blank'): def assertBookmarkURLHidden(
self, html: str, bookmark: Bookmark, link_target: str = "_blank"
):
self.assertBookmarkURLCount(html, bookmark, count=0) self.assertBookmarkURLCount(html, bookmark, count=0)
def assertNotes(self, html: str, notes_html: str, count=1): def assertNotes(self, html: str, notes_html: str, count=1):
self.assertInHTML(f''' self.assertInHTML(
f"""
<div class="notes bg-gray text-gray-dark"> <div class="notes bg-gray text-gray-dark">
<div class="notes-content"> <div class="notes-content">
{notes_html} {notes_html}
</div> </div>
</div> </div>
''', html, count=count) """,
html,
count=count,
)
def assertNotesToggle(self, html: str, count=1): def assertNotesToggle(self, html: str, count=1):
self.assertInHTML(f''' self.assertInHTML(
f"""
<button type="button" class="btn btn-link btn-sm btn-icon toggle-notes"> <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"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-note"></use> <use xlink:href="#ld-icon-note"></use>
</svg> </svg>
Notes Notes
</button> </button>
''', html, count=count) """,
html,
count=count,
)
def assertUnshareButton(self, html: str, bookmark: Bookmark, count=1): def assertUnshareButton(self, html: str, bookmark: Bookmark, count=1):
self.assertInHTML(f''' self.assertInHTML(
f"""
<button type="submit" name="unshare" value="{bookmark.id}" <button type="submit" name="unshare" value="{bookmark.id}"
class="btn btn-link btn-sm btn-icon" class="btn btn-link btn-sm btn-icon"
ld-confirm-button confirm-icon="ld-icon-unshare" confirm-question="Unshare?"> ld-confirm-button confirm-icon="ld-icon-unshare" confirm-question="Unshare?">
@ -137,10 +188,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
</svg> </svg>
Shared Shared
</button> </button>
''', html, count=count) """,
html,
count=count,
)
def assertMarkAsReadButton(self, html: str, bookmark: Bookmark, count=1): def assertMarkAsReadButton(self, html: str, bookmark: Bookmark, count=1):
self.assertInHTML(f''' self.assertInHTML(
f"""
<button type="submit" name="mark_as_read" value="{bookmark.id}" <button type="submit" name="mark_as_read" value="{bookmark.id}"
class="btn btn-link btn-sm btn-icon" class="btn btn-link btn-sm btn-icon"
ld-confirm-button confirm-icon="ld-icon-read" confirm-question="Mark as read?"> ld-confirm-button confirm-icon="ld-icon-read" confirm-question="Mark as read?">
@ -149,12 +204,19 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
</svg> </svg>
Unread Unread
</button> </button>
''', html, count=count) """,
html,
count=count,
)
def render_template(self, def render_template(
url='/bookmarks', self,
context_type: Type[contexts.BookmarkListContext] = contexts.ActiveBookmarkListContext, url="/bookmarks",
user: User | AnonymousUser = None) -> str: context_type: Type[
contexts.BookmarkListContext
] = contexts.ActiveBookmarkListContext,
user: User | AnonymousUser = None,
) -> str:
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) request = rf.get(url)
request.user = user or self.get_or_create_test_user() request.user = user or self.get_or_create_test_user()
@ -162,14 +224,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
middleware(request) middleware(request)
bookmark_list_context = context_type(request) bookmark_list_context = context_type(request)
context = RequestContext(request, {'bookmark_list': bookmark_list_context}) context = RequestContext(request, {"bookmark_list": bookmark_list_context})
template = Template( template = Template("{% include 'bookmarks/bookmark_list.html' %}")
"{% include 'bookmarks/bookmark_list.html' %}"
)
return template.render(context) return template.render(context)
def setup_date_format_test(self, date_display_setting: str, web_archive_url: str = ''): def setup_date_format_test(
self, date_display_setting: str, web_archive_url: str = ""
):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.date_added = timezone.now() - relativedelta(days=8) bookmark.date_added = timezone.now() - relativedelta(days=8)
bookmark.web_archive_snapshot_url = web_archive_url bookmark.web_archive_snapshot_url = web_archive_url
@ -180,38 +242,46 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
return bookmark return bookmark
def test_should_respect_absolute_date_setting(self): def test_should_respect_absolute_date_setting(self):
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE) bookmark = self.setup_date_format_test(
UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE
)
html = self.render_template() html = self.render_template()
formatted_date = formats.date_format(bookmark.date_added, 'SHORT_DATE_FORMAT') formatted_date = formats.date_format(bookmark.date_added, "SHORT_DATE_FORMAT")
self.assertDateLabel(html, formatted_date) self.assertDateLabel(html, formatted_date)
def test_should_render_web_archive_link_with_absolute_date_setting(self): def test_should_render_web_archive_link_with_absolute_date_setting(self):
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE, bookmark = self.setup_date_format_test(
'https://web.archive.org/web/20210811214511/https://wanikani.com/') UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE,
"https://web.archive.org/web/20210811214511/https://wanikani.com/",
)
html = self.render_template() html = self.render_template()
formatted_date = formats.date_format(bookmark.date_added, 'SHORT_DATE_FORMAT') formatted_date = formats.date_format(bookmark.date_added, "SHORT_DATE_FORMAT")
self.assertWebArchiveLink(html, formatted_date, bookmark.web_archive_snapshot_url) self.assertWebArchiveLink(
html, formatted_date, bookmark.web_archive_snapshot_url
)
def test_should_respect_relative_date_setting(self): def test_should_respect_relative_date_setting(self):
self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE) self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE)
html = self.render_template() html = self.render_template()
self.assertDateLabel(html, '1 week ago') self.assertDateLabel(html, "1 week ago")
def test_should_render_web_archive_link_with_relative_date_setting(self): def test_should_render_web_archive_link_with_relative_date_setting(self):
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE, bookmark = self.setup_date_format_test(
'https://web.archive.org/web/20210811214511/https://wanikani.com/') UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
"https://web.archive.org/web/20210811214511/https://wanikani.com/",
)
html = self.render_template() html = self.render_template()
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url) self.assertWebArchiveLink(html, "1 week ago", bookmark.web_archive_snapshot_url)
def test_bookmark_link_target_should_be_blank_by_default(self): def test_bookmark_link_target_should_be_blank_by_default(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
html = self.render_template() html = self.render_template()
self.assertBookmarksLink(html, bookmark, link_target='_blank') self.assertBookmarksLink(html, bookmark, link_target="_blank")
def test_bookmark_link_target_should_respect_user_profile(self): def test_bookmark_link_target_should_respect_user_profile(self):
profile = self.get_or_create_test_user().profile profile = self.get_or_create_test_user().profile
@ -221,17 +291,19 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
html = self.render_template() html = self.render_template()
self.assertBookmarksLink(html, bookmark, link_target='_self') self.assertBookmarksLink(html, bookmark, link_target="_self")
def test_web_archive_link_target_should_be_blank_by_default(self): def test_web_archive_link_target_should_be_blank_by_default(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.date_added = timezone.now() - relativedelta(days=8) bookmark.date_added = timezone.now() - relativedelta(days=8)
bookmark.web_archive_snapshot_url = 'https://example.com' bookmark.web_archive_snapshot_url = "https://example.com"
bookmark.save() bookmark.save()
html = self.render_template() html = self.render_template()
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank') self.assertWebArchiveLink(
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_blank"
)
def test_web_archive_link_target_should_respect_user_profile(self): def test_web_archive_link_target_should_respect_user_profile(self):
profile = self.get_or_create_test_user().profile profile = self.get_or_create_test_user().profile
@ -240,12 +312,14 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.date_added = timezone.now() - relativedelta(days=8) bookmark.date_added = timezone.now() - relativedelta(days=8)
bookmark.web_archive_snapshot_url = 'https://example.com' bookmark.web_archive_snapshot_url = "https://example.com"
bookmark.save() bookmark.save()
html = self.render_template() html = self.render_template()
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_self') self.assertWebArchiveLink(
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_self"
)
def test_should_reflect_unread_state_as_css_class(self): def test_should_reflect_unread_state_as_css_class(self):
self.setup_bookmark(unread=True) self.setup_bookmark(unread=True)
@ -281,7 +355,9 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.assertNoShareInfo(html, bookmark) self.assertNoShareInfo(html, bookmark)
def test_show_share_info_for_non_owned_bookmarks(self): def test_show_share_info_for_non_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user.profile.enable_sharing = True other_user.profile.enable_sharing = True
other_user.profile.save() other_user.profile.save()
@ -292,25 +368,32 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.assertShareInfo(html, bookmark) self.assertShareInfo(html, bookmark)
def test_share_info_user_link_keeps_query_params(self): def test_share_info_user_link_keeps_query_params(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
other_user.profile.enable_sharing = True other_user.profile.enable_sharing = True
other_user.profile.save() other_user.profile.save()
bookmark = self.setup_bookmark(user=other_user, shared=True, title='foo') bookmark = self.setup_bookmark(user=other_user, shared=True, title="foo")
html = self.render_template(url='/bookmarks?q=foo', context_type=contexts.SharedBookmarkListContext) html = self.render_template(
url="/bookmarks?q=foo", context_type=contexts.SharedBookmarkListContext
)
self.assertInHTML(f''' self.assertInHTML(
f"""
<span>Shared by <span>Shared by
<a href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a> <a href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a>
</span> </span>
''', html) """,
html,
)
def test_favicon_should_be_visible_when_favicons_enabled(self): def test_favicon_should_be_visible_when_favicons_enabled(self):
profile = self.get_or_create_test_user().profile profile = self.get_or_create_test_user().profile
profile.enable_favicons = True profile.enable_favicons = True
profile.save() profile.save()
bookmark = self.setup_bookmark(favicon_file='https_example_com.png') bookmark = self.setup_bookmark(favicon_file="https_example_com.png")
html = self.render_template() html = self.render_template()
self.assertFaviconVisible(html, bookmark) self.assertFaviconVisible(html, bookmark)
@ -320,7 +403,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
profile.enable_favicons = True profile.enable_favicons = True
profile.save() profile.save()
bookmark = self.setup_bookmark(favicon_file='') bookmark = self.setup_bookmark(favicon_file="")
html = self.render_template() html = self.render_template()
self.assertFaviconHidden(html, bookmark) self.assertFaviconHidden(html, bookmark)
@ -330,7 +413,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
profile.enable_favicons = False profile.enable_favicons = False
profile.save() profile.save()
bookmark = self.setup_bookmark(favicon_file='https_example_com.png') bookmark = self.setup_bookmark(favicon_file="https_example_com.png")
html = self.render_template() html = self.render_template()
self.assertFaviconHidden(html, bookmark) self.assertFaviconHidden(html, bookmark)
@ -428,21 +511,23 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.setup_bookmark() self.setup_bookmark()
html = self.render_template() html = self.render_template()
self.assertNotes(html, '', 0) self.assertNotes(html, "", 0)
self.assertNotesToggle(html, 0) self.assertNotesToggle(html, 0)
def test_with_notes(self): def test_with_notes(self):
self.setup_bookmark(notes='Test note') self.setup_bookmark(notes="Test note")
html = self.render_template() html = self.render_template()
note_html = '<p>Test note</p>' note_html = "<p>Test note</p>"
self.assertNotes(html, note_html, 1) self.assertNotes(html, note_html, 1)
def test_note_renders_markdown(self): def test_note_renders_markdown(self):
self.setup_bookmark(notes='**Example:** `print("Hello world!")`') self.setup_bookmark(notes='**Example:** `print("Hello world!")`')
html = self.render_template() html = self.render_template()
note_html = '<p><strong>Example:</strong> <code>print("Hello world!")</code></p>' note_html = (
'<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
)
self.assertNotes(html, note_html, 1) self.assertNotes(html, note_html, 1)
def test_note_cleans_html(self): def test_note_cleans_html(self):
@ -453,7 +538,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
self.assertNotes(html, note_html, 1) self.assertNotes(html, note_html, 1)
def test_notes_are_hidden_initially_by_default(self): def test_notes_are_hidden_initially_by_default(self):
self.setup_bookmark(notes='Test note') self.setup_bookmark(notes="Test note")
html = collapse_whitespace(self.render_template()) html = collapse_whitespace(self.render_template())
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html) self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
@ -463,7 +548,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
profile.permanent_notes = False profile.permanent_notes = False
profile.save() profile.save()
self.setup_bookmark(notes='Test note') self.setup_bookmark(notes="Test note")
html = collapse_whitespace(self.render_template()) html = collapse_whitespace(self.render_template())
self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html) self.assertIn('<ul class="bookmark-list" data-bookmarks-total="1">', html)
@ -473,13 +558,15 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
profile.permanent_notes = True profile.permanent_notes = True
profile.save() profile.save()
self.setup_bookmark(notes='Test note') self.setup_bookmark(notes="Test note")
html = collapse_whitespace(self.render_template()) html = collapse_whitespace(self.render_template())
self.assertIn('<ul class="bookmark-list show-notes" data-bookmarks-total="1">', html) self.assertIn(
'<ul class="bookmark-list show-notes" data-bookmarks-total="1">', html
)
def test_toggle_notes_is_visible_by_default(self): def test_toggle_notes_is_visible_by_default(self):
self.setup_bookmark(notes='Test note') self.setup_bookmark(notes="Test note")
html = self.render_template() html = self.render_template()
self.assertNotesToggle(html, 1) self.assertNotesToggle(html, 1)
@ -489,7 +576,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
profile.permanent_notes = False profile.permanent_notes = False
profile.save() profile.save()
self.setup_bookmark(notes='Test note') self.setup_bookmark(notes="Test note")
html = self.render_template() html = self.render_template()
self.assertNotesToggle(html, 1) self.assertNotesToggle(html, 1)
@ -499,7 +586,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
profile.permanent_notes = True profile.permanent_notes = True
profile.save() profile.save()
self.setup_bookmark(notes='Test note') self.setup_bookmark(notes="Test note")
html = self.render_template() html = self.render_template()
self.assertNotesToggle(html, 0) self.assertNotesToggle(html, 0)
@ -512,25 +599,35 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.date_added = timezone.now() - relativedelta(days=8) bookmark.date_added = timezone.now() - relativedelta(days=8)
bookmark.web_archive_snapshot_url = 'https://web.archive.org/web/20230531200136/https://example.com' bookmark.web_archive_snapshot_url = (
"https://web.archive.org/web/20230531200136/https://example.com"
)
bookmark.notes = '**Example:** `print("Hello world!")`' bookmark.notes = '**Example:** `print("Hello world!")`'
bookmark.favicon_file = 'https_example_com.png' bookmark.favicon_file = "https_example_com.png"
bookmark.shared = True bookmark.shared = True
bookmark.unread = True bookmark.unread = True
bookmark.save() bookmark.save()
html = self.render_template(context_type=contexts.SharedBookmarkListContext, user=AnonymousUser()) html = self.render_template(
self.assertBookmarksLink(html, bookmark, link_target='_blank') context_type=contexts.SharedBookmarkListContext, user=AnonymousUser()
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank') )
self.assertBookmarksLink(html, bookmark, link_target="_blank")
self.assertWebArchiveLink(
html, "1 week ago", bookmark.web_archive_snapshot_url, link_target="_blank"
)
self.assertNoBookmarkActions(html, bookmark) self.assertNoBookmarkActions(html, bookmark)
self.assertShareInfo(html, bookmark) self.assertShareInfo(html, bookmark)
self.assertMarkAsReadButton(html, bookmark, count=0) self.assertMarkAsReadButton(html, bookmark, count=0)
self.assertUnshareButton(html, bookmark, count=0) self.assertUnshareButton(html, bookmark, count=0)
note_html = '<p><strong>Example:</strong> <code>print("Hello world!")</code></p>' note_html = (
'<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
)
self.assertNotes(html, note_html, 1) self.assertNotes(html, note_html, 1)
self.assertFaviconVisible(html, bookmark) self.assertFaviconVisible(html, bookmark)
def test_empty_state(self): def test_empty_state(self):
html = self.render_template() html = self.render_template()
self.assertInHTML('<p class="empty-title h5">You have no bookmarks yet</p>', html) self.assertInHTML(
'<p class="empty-title h5">You have no bookmarks yet</p>', html
)

View file

@ -6,11 +6,17 @@ from bookmarks.models import Bookmark
class BookmarkTestCase(TestCase): class BookmarkTestCase(TestCase):
def test_bookmark_resolved_title(self): def test_bookmark_resolved_title(self):
bookmark = Bookmark(title='Custom title', website_title='Website title', url='https://example.com') bookmark = Bookmark(
self.assertEqual(bookmark.resolved_title, 'Custom title') title="Custom title",
website_title="Website title",
url="https://example.com",
)
self.assertEqual(bookmark.resolved_title, "Custom title")
bookmark = Bookmark(title='', website_title='Website title', url='https://example.com') bookmark = Bookmark(
self.assertEqual(bookmark.resolved_title, 'Website title') title="", website_title="Website title", url="https://example.com"
)
self.assertEqual(bookmark.resolved_title, "Website title")
bookmark = Bookmark(title='', website_title='', url='https://example.com') bookmark = Bookmark(title="", website_title="", url="https://example.com")
self.assertEqual(bookmark.resolved_title, 'https://example.com') self.assertEqual(bookmark.resolved_title, "https://example.com")

View file

@ -7,9 +7,21 @@ from django.utils import timezone
from bookmarks.models import Bookmark, Tag from bookmarks.models import Bookmark, Tag
from bookmarks.services import tasks from bookmarks.services import tasks
from bookmarks.services import website_loader from bookmarks.services import website_loader
from bookmarks.services.bookmarks import create_bookmark, update_bookmark, archive_bookmark, archive_bookmarks, \ from bookmarks.services.bookmarks import (
unarchive_bookmark, unarchive_bookmarks, delete_bookmarks, tag_bookmarks, untag_bookmarks, mark_bookmarks_as_read, \ create_bookmark,
mark_bookmarks_as_unread, share_bookmarks, unshare_bookmarks update_bookmark,
archive_bookmark,
archive_bookmarks,
unarchive_bookmark,
unarchive_bookmarks,
delete_bookmarks,
tag_bookmarks,
untag_bookmarks,
mark_bookmarks_as_read,
mark_bookmarks_as_unread,
share_bookmarks,
unshare_bookmarks,
)
from bookmarks.services.website_loader import WebsiteMetadata from bookmarks.services.website_loader import WebsiteMetadata
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
@ -22,36 +34,48 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.get_or_create_test_user() self.get_or_create_test_user()
def test_create_should_update_website_metadata(self): def test_create_should_update_website_metadata(self):
with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata: with patch.object(
website_loader, "load_website_metadata"
) as mock_load_website_metadata:
expected_metadata = WebsiteMetadata( expected_metadata = WebsiteMetadata(
'https://example.com', "https://example.com", "Website title", "Website description"
'Website title',
'Website description'
) )
mock_load_website_metadata.return_value = expected_metadata mock_load_website_metadata.return_value = expected_metadata
bookmark_data = Bookmark(url='https://example.com', bookmark_data = Bookmark(
title='Updated Title', url="https://example.com",
description='Updated description', title="Updated Title",
description="Updated description",
unread=True, unread=True,
shared=True, shared=True,
is_archived=True) is_archived=True,
created_bookmark = create_bookmark(bookmark_data, '', self.get_or_create_test_user()) )
created_bookmark = create_bookmark(
bookmark_data, "", self.get_or_create_test_user()
)
created_bookmark.refresh_from_db() created_bookmark.refresh_from_db()
self.assertEqual(expected_metadata.title, created_bookmark.website_title) self.assertEqual(expected_metadata.title, created_bookmark.website_title)
self.assertEqual(expected_metadata.description, created_bookmark.website_description) self.assertEqual(
expected_metadata.description, created_bookmark.website_description
)
def test_create_should_update_existing_bookmark_with_same_url(self): def test_create_should_update_existing_bookmark_with_same_url(self):
original_bookmark = self.setup_bookmark(url='https://example.com', unread=False, shared=False) original_bookmark = self.setup_bookmark(
bookmark_data = Bookmark(url='https://example.com', url="https://example.com", unread=False, shared=False
title='Updated Title', )
description='Updated description', bookmark_data = Bookmark(
notes='Updated notes', url="https://example.com",
title="Updated Title",
description="Updated description",
notes="Updated notes",
unread=True, unread=True,
shared=True, shared=True,
is_archived=True) is_archived=True,
updated_bookmark = create_bookmark(bookmark_data, '', self.get_or_create_test_user()) )
updated_bookmark = create_bookmark(
bookmark_data, "", self.get_or_create_test_user()
)
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(updated_bookmark.id, original_bookmark.id) self.assertEqual(updated_bookmark.id, original_bookmark.id)
@ -64,75 +88,91 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertFalse(updated_bookmark.is_archived) self.assertFalse(updated_bookmark.is_archived)
def test_create_should_create_web_archive_snapshot(self): def test_create_should_create_web_archive_snapshot(self):
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot: with patch.object(
bookmark_data = Bookmark(url='https://example.com') tasks, "create_web_archive_snapshot"
bookmark = create_bookmark(bookmark_data, 'tag1,tag2', self.user) ) as mock_create_web_archive_snapshot:
bookmark_data = Bookmark(url="https://example.com")
bookmark = create_bookmark(bookmark_data, "tag1,tag2", self.user)
mock_create_web_archive_snapshot.assert_called_once_with(self.user, bookmark, False) mock_create_web_archive_snapshot.assert_called_once_with(
self.user, bookmark, False
)
def test_create_should_load_favicon(self): def test_create_should_load_favicon(self):
with patch.object(tasks, 'load_favicon') as mock_load_favicon: with patch.object(tasks, "load_favicon") as mock_load_favicon:
bookmark_data = Bookmark(url='https://example.com') bookmark_data = Bookmark(url="https://example.com")
bookmark = create_bookmark(bookmark_data, 'tag1,tag2', self.user) bookmark = create_bookmark(bookmark_data, "tag1,tag2", self.user)
mock_load_favicon.assert_called_once_with(self.user, bookmark) mock_load_favicon.assert_called_once_with(self.user, bookmark)
def test_update_should_create_web_archive_snapshot_if_url_did_change(self): def test_update_should_create_web_archive_snapshot_if_url_did_change(self):
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot: with patch.object(
tasks, "create_web_archive_snapshot"
) as mock_create_web_archive_snapshot:
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.url = 'https://example.com/updated' bookmark.url = "https://example.com/updated"
update_bookmark(bookmark, 'tag1,tag2', self.user) update_bookmark(bookmark, "tag1,tag2", self.user)
mock_create_web_archive_snapshot.assert_called_once_with(self.user, bookmark, True) mock_create_web_archive_snapshot.assert_called_once_with(
self.user, bookmark, True
)
def test_update_should_not_create_web_archive_snapshot_if_url_did_not_change(self): def test_update_should_not_create_web_archive_snapshot_if_url_did_not_change(self):
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot: with patch.object(
tasks, "create_web_archive_snapshot"
) as mock_create_web_archive_snapshot:
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.title = 'updated title' bookmark.title = "updated title"
update_bookmark(bookmark, 'tag1,tag2', self.user) update_bookmark(bookmark, "tag1,tag2", self.user)
mock_create_web_archive_snapshot.assert_not_called() mock_create_web_archive_snapshot.assert_not_called()
def test_update_should_update_website_metadata_if_url_did_change(self): def test_update_should_update_website_metadata_if_url_did_change(self):
with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata: with patch.object(
website_loader, "load_website_metadata"
) as mock_load_website_metadata:
expected_metadata = WebsiteMetadata( expected_metadata = WebsiteMetadata(
'https://example.com/updated', "https://example.com/updated",
'Updated website title', "Updated website title",
'Updated website description' "Updated website description",
) )
mock_load_website_metadata.return_value = expected_metadata mock_load_website_metadata.return_value = expected_metadata
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.url = 'https://example.com/updated' bookmark.url = "https://example.com/updated"
update_bookmark(bookmark, 'tag1,tag2', self.user) update_bookmark(bookmark, "tag1,tag2", self.user)
bookmark.refresh_from_db() bookmark.refresh_from_db()
mock_load_website_metadata.assert_called_once() mock_load_website_metadata.assert_called_once()
self.assertEqual(expected_metadata.title, bookmark.website_title) self.assertEqual(expected_metadata.title, bookmark.website_title)
self.assertEqual(expected_metadata.description, bookmark.website_description) self.assertEqual(
expected_metadata.description, bookmark.website_description
)
def test_update_should_not_update_website_metadata_if_url_did_not_change(self): def test_update_should_not_update_website_metadata_if_url_did_not_change(self):
with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata: with patch.object(
website_loader, "load_website_metadata"
) as mock_load_website_metadata:
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.title = 'updated title' bookmark.title = "updated title"
update_bookmark(bookmark, 'tag1,tag2', self.user) update_bookmark(bookmark, "tag1,tag2", self.user)
mock_load_website_metadata.assert_not_called() mock_load_website_metadata.assert_not_called()
def test_update_should_update_favicon(self): def test_update_should_update_favicon(self):
with patch.object(tasks, 'load_favicon') as mock_load_favicon: with patch.object(tasks, "load_favicon") as mock_load_favicon:
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.title = 'updated title' bookmark.title = "updated title"
update_bookmark(bookmark, 'tag1,tag2', self.user) update_bookmark(bookmark, "tag1,tag2", self.user)
mock_load_favicon.assert_called_once_with(self.user, bookmark) mock_load_favicon.assert_called_once_with(self.user, bookmark)
def test_archive_bookmark(self): def test_archive_bookmark(self):
bookmark = Bookmark( bookmark = Bookmark(
url='https://example.com', url="https://example.com",
date_added=timezone.now(), date_added=timezone.now(),
date_modified=timezone.now(), date_modified=timezone.now(),
owner=self.user owner=self.user,
) )
bookmark.save() bookmark.save()
@ -146,7 +186,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
def test_unarchive_bookmark(self): def test_unarchive_bookmark(self):
bookmark = Bookmark( bookmark = Bookmark(
url='https://example.com', url="https://example.com",
date_added=timezone.now(), date_added=timezone.now(),
date_modified=timezone.now(), date_modified=timezone.now(),
owner=self.user, owner=self.user,
@ -165,7 +205,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
archive_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()) archive_bookmarks(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -183,12 +225,17 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
def test_archive_bookmarks_should_only_archive_user_owned_bookmarks(self): def test_archive_bookmarks_should_only_archive_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark() bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
inaccessible_bookmark = self.setup_bookmark(user=other_user) inaccessible_bookmark = self.setup_bookmark(user=other_user)
archive_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user()) archive_bookmarks(
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
self.get_or_create_test_user(),
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -199,7 +246,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
archive_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user()) archive_bookmarks(
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
self.get_or_create_test_user(),
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -210,7 +260,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(is_archived=True) bookmark2 = self.setup_bookmark(is_archived=True)
bookmark3 = self.setup_bookmark(is_archived=True) bookmark3 = self.setup_bookmark(is_archived=True)
unarchive_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()) unarchive_bookmarks(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -221,19 +273,26 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(is_archived=True) bookmark2 = self.setup_bookmark(is_archived=True)
bookmark3 = self.setup_bookmark(is_archived=True) bookmark3 = self.setup_bookmark(is_archived=True)
unarchive_bookmarks([bookmark1.id, bookmark3.id], self.get_or_create_test_user()) unarchive_bookmarks(
[bookmark1.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
def test_unarchive_bookmarks_should_only_unarchive_user_owned_bookmarks(self): def test_unarchive_bookmarks_should_only_unarchive_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(is_archived=True) bookmark1 = self.setup_bookmark(is_archived=True)
bookmark2 = self.setup_bookmark(is_archived=True) bookmark2 = self.setup_bookmark(is_archived=True)
inaccessible_bookmark = self.setup_bookmark(is_archived=True, user=other_user) inaccessible_bookmark = self.setup_bookmark(is_archived=True, user=other_user)
unarchive_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user()) unarchive_bookmarks(
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
self.get_or_create_test_user(),
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -244,7 +303,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(is_archived=True) bookmark2 = self.setup_bookmark(is_archived=True)
bookmark3 = self.setup_bookmark(is_archived=True) bookmark3 = self.setup_bookmark(is_archived=True)
unarchive_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user()) unarchive_bookmarks(
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
self.get_or_create_test_user(),
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
@ -255,7 +317,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
delete_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()) delete_bookmarks(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
@ -273,23 +337,32 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
def test_delete_bookmarks_should_only_delete_user_owned_bookmarks(self): def test_delete_bookmarks_should_only_delete_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark() bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
inaccessible_bookmark = self.setup_bookmark(user=other_user) inaccessible_bookmark = self.setup_bookmark(user=other_user)
delete_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user()) delete_bookmarks(
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
self.get_or_create_test_user(),
)
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
self.assertIsNotNone(Bookmark.objects.filter(id=inaccessible_bookmark.id).first()) self.assertIsNotNone(
Bookmark.objects.filter(id=inaccessible_bookmark.id).first()
)
def test_delete_bookmarks_should_accept_mix_of_int_and_string_ids(self): def test_delete_bookmarks_should_accept_mix_of_int_and_string_ids(self):
bookmark1 = self.setup_bookmark() bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
delete_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()) delete_bookmarks(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
@ -302,8 +375,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}', tag_bookmarks(
self.get_or_create_test_user()) [bookmark1.id, bookmark2.id, bookmark3.id],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -318,7 +394,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
bookmark3 = self.setup_bookmark() bookmark3 = self.setup_bookmark()
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], 'tag1,tag2', self.get_or_create_test_user()) tag_bookmarks(
[bookmark1.id, bookmark2.id, bookmark3.id],
"tag1,tag2",
self.get_or_create_test_user(),
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -326,8 +406,8 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(2, Tag.objects.count()) self.assertEqual(2, Tag.objects.count())
tag1 = Tag.objects.filter(name='tag1').first() tag1 = Tag.objects.filter(name="tag1").first()
tag2 = Tag.objects.filter(name='tag2').first() tag2 = Tag.objects.filter(name="tag2").first()
self.assertIsNotNone(tag1) self.assertIsNotNone(tag1)
self.assertIsNotNone(tag2) self.assertIsNotNone(tag2)
@ -346,8 +426,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
BookmarkToTagRelationShip = Bookmark.tags.through BookmarkToTagRelationShip = Bookmark.tags.through
self.assertEqual(3, BookmarkToTagRelationShip.objects.count()) self.assertEqual(3, BookmarkToTagRelationShip.objects.count())
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}', tag_bookmarks(
self.get_or_create_test_user()) [bookmark1.id, bookmark2.id, bookmark3.id],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -365,7 +448,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
tag_bookmarks([bookmark1.id, bookmark3.id], f'{tag1.name},{tag2.name}', self.get_or_create_test_user()) tag_bookmarks(
[bookmark1.id, bookmark3.id],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -376,15 +463,20 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2]) self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
def test_tag_bookmarks_should_only_tag_user_owned_bookmarks(self): def test_tag_bookmarks_should_only_tag_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark() bookmark1 = self.setup_bookmark()
bookmark2 = self.setup_bookmark() bookmark2 = self.setup_bookmark()
inaccessible_bookmark = self.setup_bookmark(user=other_user) inaccessible_bookmark = self.setup_bookmark(user=other_user)
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
tag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name},{tag2.name}', tag_bookmarks(
self.get_or_create_test_user()) [bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -401,8 +493,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
tag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name},{tag2.name}', tag_bookmarks(
self.get_or_create_test_user()) [str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2]) self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2])
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2]) self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
@ -415,8 +510,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(tags=[tag1, tag2]) bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
bookmark3 = self.setup_bookmark(tags=[tag1, tag2]) bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
untag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}', untag_bookmarks(
self.get_or_create_test_user()) [bookmark1.id, bookmark2.id, bookmark3.id],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -433,7 +531,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(tags=[tag1, tag2]) bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
bookmark3 = self.setup_bookmark(tags=[tag1, tag2]) bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
untag_bookmarks([bookmark1.id, bookmark3.id], f'{tag1.name},{tag2.name}', self.get_or_create_test_user()) untag_bookmarks(
[bookmark1.id, bookmark3.id],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -444,15 +546,20 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertCountEqual(bookmark3.tags.all(), []) self.assertCountEqual(bookmark3.tags.all(), [])
def test_untag_bookmarks_should_only_tag_user_owned_bookmarks(self): def test_untag_bookmarks_should_only_tag_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
tag1 = self.setup_tag() tag1 = self.setup_tag()
tag2 = self.setup_tag() tag2 = self.setup_tag()
bookmark1 = self.setup_bookmark(tags=[tag1, tag2]) bookmark1 = self.setup_bookmark(tags=[tag1, tag2])
bookmark2 = self.setup_bookmark(tags=[tag1, tag2]) bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
inaccessible_bookmark = self.setup_bookmark(user=other_user, tags=[tag1, tag2]) inaccessible_bookmark = self.setup_bookmark(user=other_user, tags=[tag1, tag2])
untag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name},{tag2.name}', untag_bookmarks(
self.get_or_create_test_user()) [bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
bookmark1.refresh_from_db() bookmark1.refresh_from_db()
bookmark2.refresh_from_db() bookmark2.refresh_from_db()
@ -469,8 +576,11 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(tags=[tag1, tag2]) bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
bookmark3 = self.setup_bookmark(tags=[tag1, tag2]) bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
untag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name},{tag2.name}', untag_bookmarks(
self.get_or_create_test_user()) [str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
f"{tag1.name},{tag2.name}",
self.get_or_create_test_user(),
)
self.assertCountEqual(bookmark1.tags.all(), []) self.assertCountEqual(bookmark1.tags.all(), [])
self.assertCountEqual(bookmark2.tags.all(), []) self.assertCountEqual(bookmark2.tags.all(), [])
@ -481,7 +591,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(unread=True) bookmark2 = self.setup_bookmark(unread=True)
bookmark3 = self.setup_bookmark(unread=True) bookmark3 = self.setup_bookmark(unread=True)
mark_bookmarks_as_read([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()) mark_bookmarks_as_read(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
@ -492,19 +604,26 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(unread=True) bookmark2 = self.setup_bookmark(unread=True)
bookmark3 = self.setup_bookmark(unread=True) bookmark3 = self.setup_bookmark(unread=True)
mark_bookmarks_as_read([bookmark1.id, bookmark3.id], self.get_or_create_test_user()) mark_bookmarks_as_read(
[bookmark1.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread)
def test_mark_bookmarks_as_read_should_only_update_user_owned_bookmarks(self): 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 = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(unread=True) bookmark1 = self.setup_bookmark(unread=True)
bookmark2 = self.setup_bookmark(unread=True) bookmark2 = self.setup_bookmark(unread=True)
inaccessible_bookmark = self.setup_bookmark(unread=True, user=other_user) inaccessible_bookmark = self.setup_bookmark(unread=True, user=other_user)
mark_bookmarks_as_read([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user()) mark_bookmarks_as_read(
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
self.get_or_create_test_user(),
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
@ -515,7 +634,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(unread=True) bookmark2 = self.setup_bookmark(unread=True)
bookmark3 = self.setup_bookmark(unread=True) bookmark3 = self.setup_bookmark(unread=True)
mark_bookmarks_as_read([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user()) mark_bookmarks_as_read(
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
self.get_or_create_test_user(),
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
@ -526,7 +648,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(unread=False) bookmark2 = self.setup_bookmark(unread=False)
bookmark3 = self.setup_bookmark(unread=False) bookmark3 = self.setup_bookmark(unread=False)
mark_bookmarks_as_unread([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()) mark_bookmarks_as_unread(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
@ -537,19 +661,26 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(unread=False) bookmark2 = self.setup_bookmark(unread=False)
bookmark3 = self.setup_bookmark(unread=False) bookmark3 = self.setup_bookmark(unread=False)
mark_bookmarks_as_unread([bookmark1.id, bookmark3.id], self.get_or_create_test_user()) mark_bookmarks_as_unread(
[bookmark1.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).unread)
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread)
def test_mark_bookmarks_as_unread_should_only_update_user_owned_bookmarks(self): 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 = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(unread=False) bookmark1 = self.setup_bookmark(unread=False)
bookmark2 = self.setup_bookmark(unread=False) bookmark2 = self.setup_bookmark(unread=False)
inaccessible_bookmark = self.setup_bookmark(unread=False, user=other_user) inaccessible_bookmark = self.setup_bookmark(unread=False, user=other_user)
mark_bookmarks_as_unread([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user()) mark_bookmarks_as_unread(
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
self.get_or_create_test_user(),
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
@ -560,7 +691,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(unread=False) bookmark2 = self.setup_bookmark(unread=False)
bookmark3 = self.setup_bookmark(unread=False) bookmark3 = self.setup_bookmark(unread=False)
mark_bookmarks_as_unread([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user()) mark_bookmarks_as_unread(
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
self.get_or_create_test_user(),
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).unread)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).unread)
@ -571,7 +705,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(shared=False) bookmark2 = self.setup_bookmark(shared=False)
bookmark3 = self.setup_bookmark(shared=False) bookmark3 = self.setup_bookmark(shared=False)
share_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()) share_bookmarks(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
@ -589,12 +725,17 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared)
def test_share_bookmarks_should_only_update_user_owned_bookmarks(self): def test_share_bookmarks_should_only_update_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(shared=False) bookmark1 = self.setup_bookmark(shared=False)
bookmark2 = self.setup_bookmark(shared=False) bookmark2 = self.setup_bookmark(shared=False)
inaccessible_bookmark = self.setup_bookmark(shared=False, user=other_user) inaccessible_bookmark = self.setup_bookmark(shared=False, user=other_user)
share_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user()) share_bookmarks(
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
self.get_or_create_test_user(),
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
@ -605,7 +746,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(shared=False) bookmark2 = self.setup_bookmark(shared=False)
bookmark3 = self.setup_bookmark(shared=False) bookmark3 = self.setup_bookmark(shared=False)
share_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user()) share_bookmarks(
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
self.get_or_create_test_user(),
)
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark2.id).shared)
@ -616,7 +760,9 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(shared=True) bookmark2 = self.setup_bookmark(shared=True)
bookmark3 = self.setup_bookmark(shared=True) bookmark3 = self.setup_bookmark(shared=True)
unshare_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()) unshare_bookmarks(
[bookmark1.id, bookmark2.id, bookmark3.id], self.get_or_create_test_user()
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
@ -634,12 +780,17 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared)
def test_unshare_bookmarks_should_only_update_user_owned_bookmarks(self): def test_unshare_bookmarks_should_only_update_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
bookmark1 = self.setup_bookmark(shared=True) bookmark1 = self.setup_bookmark(shared=True)
bookmark2 = self.setup_bookmark(shared=True) bookmark2 = self.setup_bookmark(shared=True)
inaccessible_bookmark = self.setup_bookmark(shared=True, user=other_user) inaccessible_bookmark = self.setup_bookmark(shared=True, user=other_user)
unshare_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], self.get_or_create_test_user()) unshare_bookmarks(
[bookmark1.id, bookmark2.id, inaccessible_bookmark.id],
self.get_or_create_test_user(),
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)
@ -650,7 +801,10 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
bookmark2 = self.setup_bookmark(shared=True) bookmark2 = self.setup_bookmark(shared=True)
bookmark3 = self.setup_bookmark(shared=True) bookmark3 = self.setup_bookmark(shared=True)
unshare_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], self.get_or_create_test_user()) unshare_bookmarks(
[str(bookmark1.id), bookmark2.id, str(bookmark3.id)],
self.get_or_create_test_user(),
)
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark1.id).shared)
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark2.id).shared)

View file

@ -16,8 +16,10 @@ from bookmarks.services import tasks
from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging
def create_wayback_machine_save_api_mock(archive_url: str = 'https://example.com/created_snapshot', def create_wayback_machine_save_api_mock(
fail_on_save: bool = False): archive_url: str = "https://example.com/created_snapshot",
fail_on_save: bool = False,
):
mock_api = mock.Mock(archive_url=archive_url) mock_api = mock.Mock(archive_url=archive_url)
if fail_on_save: if fail_on_save:
@ -32,14 +34,18 @@ class MockCdxSnapshot:
datetime_timestamp: datetime.datetime datetime_timestamp: datetime.datetime
def create_cdx_server_api_mock(archive_url: str | None = 'https://example.com/newest_snapshot', def create_cdx_server_api_mock(
fail_loading_snapshot=False): archive_url: str | None = "https://example.com/newest_snapshot",
fail_loading_snapshot=False,
):
mock_api = mock.Mock() mock_api = mock.Mock()
if fail_loading_snapshot: if fail_loading_snapshot:
mock_api.newest.side_effect = WaybackError mock_api.newest.side_effect = WaybackError
elif archive_url: elif archive_url:
mock_api.newest.return_value = MockCdxSnapshot(archive_url, datetime.datetime.now()) mock_api.newest.return_value = MockCdxSnapshot(
archive_url, datetime.datetime.now()
)
else: else:
mock_api.newest.return_value = None mock_api.newest.return_value = None
@ -50,13 +56,15 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self): def setUp(self):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
user.profile.web_archive_integration = UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED user.profile.web_archive_integration = (
UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED
)
user.profile.enable_favicons = True user.profile.enable_favicons = True
user.profile.save() user.profile.save()
@disable_logging @disable_logging
def run_pending_task(self, task_function: Any): def run_pending_task(self, task_function: Any):
func = getattr(task_function, 'task_function', None) func = getattr(task_function, "task_function", None)
task = Task.objects.all()[0] task = Task.objects.all()[0]
self.assertEqual(task_function.name, task.task_name) self.assertEqual(task_function.name, task.task_name)
args, kwargs = task.params() args, kwargs = task.params()
@ -65,7 +73,7 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
@disable_logging @disable_logging
def run_all_pending_tasks(self, task_function: Any): def run_all_pending_tasks(self, task_function: Any):
func = getattr(task_function, 'task_function', None) func = getattr(task_function, "task_function", None)
tasks = Task.objects.all() tasks = Task.objects.all()
for task in tasks: for task in tasks:
@ -78,86 +86,129 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
mock_save_api = create_wayback_machine_save_api_mock() mock_save_api = create_wayback_machine_save_api_mock()
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api): with mock.patch.object(
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False) waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api
):
tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, False
)
self.run_pending_task(tasks._create_web_archive_snapshot_task) self.run_pending_task(tasks._create_web_archive_snapshot_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
mock_save_api.save.assert_called_once() mock_save_api.save.assert_called_once()
self.assertEqual(bookmark.web_archive_snapshot_url, 'https://example.com/created_snapshot') self.assertEqual(
bookmark.web_archive_snapshot_url,
"https://example.com/created_snapshot",
)
def test_create_web_archive_snapshot_should_handle_missing_bookmark_id(self): def test_create_web_archive_snapshot_should_handle_missing_bookmark_id(self):
mock_save_api = create_wayback_machine_save_api_mock() mock_save_api = create_wayback_machine_save_api_mock()
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api): with mock.patch.object(
waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api
):
tasks._create_web_archive_snapshot_task(123, False) tasks._create_web_archive_snapshot_task(123, False)
self.run_pending_task(tasks._create_web_archive_snapshot_task) self.run_pending_task(tasks._create_web_archive_snapshot_task)
mock_save_api.save.assert_not_called() mock_save_api.save.assert_not_called()
def test_create_web_archive_snapshot_should_skip_if_snapshot_exists(self): def test_create_web_archive_snapshot_should_skip_if_snapshot_exists(self):
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com') bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com")
mock_save_api = create_wayback_machine_save_api_mock() mock_save_api = create_wayback_machine_save_api_mock()
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api): with mock.patch.object(
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False) waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api
):
tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, False
)
self.run_pending_task(tasks._create_web_archive_snapshot_task) self.run_pending_task(tasks._create_web_archive_snapshot_task)
mock_save_api.assert_not_called() mock_save_api.assert_not_called()
def test_create_web_archive_snapshot_should_force_update_snapshot(self): def test_create_web_archive_snapshot_should_force_update_snapshot(self):
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com') bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com")
mock_save_api = create_wayback_machine_save_api_mock(archive_url='https://other.com') mock_save_api = create_wayback_machine_save_api_mock(
archive_url="https://other.com"
)
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api): with mock.patch.object(
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, True) waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api
):
tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, True
)
self.run_pending_task(tasks._create_web_archive_snapshot_task) self.run_pending_task(tasks._create_web_archive_snapshot_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual(bookmark.web_archive_snapshot_url, 'https://other.com') self.assertEqual(bookmark.web_archive_snapshot_url, "https://other.com")
def test_create_web_archive_snapshot_should_use_newest_snapshot_as_fallback(self): def test_create_web_archive_snapshot_should_use_newest_snapshot_as_fallback(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True) mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True)
mock_cdx_api = create_cdx_server_api_mock() mock_cdx_api = create_cdx_server_api_mock()
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api): with mock.patch.object(
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api
return_value=mock_cdx_api): ):
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False) with mock.patch.object(
bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, False
)
self.run_pending_task(tasks._create_web_archive_snapshot_task) self.run_pending_task(tasks._create_web_archive_snapshot_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
mock_cdx_api.newest.assert_called_once() mock_cdx_api.newest.assert_called_once()
self.assertEqual('https://example.com/newest_snapshot', bookmark.web_archive_snapshot_url) self.assertEqual(
"https://example.com/newest_snapshot",
bookmark.web_archive_snapshot_url,
)
def test_create_web_archive_snapshot_should_ignore_missing_newest_snapshot(self): def test_create_web_archive_snapshot_should_ignore_missing_newest_snapshot(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True) mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True)
mock_cdx_api = create_cdx_server_api_mock(archive_url=None) mock_cdx_api = create_cdx_server_api_mock(archive_url=None)
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api): with mock.patch.object(
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api
return_value=mock_cdx_api): ):
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False) with mock.patch.object(
bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, False
)
self.run_pending_task(tasks._create_web_archive_snapshot_task) self.run_pending_task(tasks._create_web_archive_snapshot_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual('', bookmark.web_archive_snapshot_url) self.assertEqual("", bookmark.web_archive_snapshot_url)
def test_create_web_archive_snapshot_should_ignore_newest_snapshot_errors(self): def test_create_web_archive_snapshot_should_ignore_newest_snapshot_errors(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True) mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True)
mock_cdx_api = create_cdx_server_api_mock(fail_loading_snapshot=True) mock_cdx_api = create_cdx_server_api_mock(fail_loading_snapshot=True)
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api): with mock.patch.object(
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api
return_value=mock_cdx_api): ):
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False) with mock.patch.object(
bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, False
)
self.run_pending_task(tasks._create_web_archive_snapshot_task) self.run_pending_task(tasks._create_web_archive_snapshot_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual('', bookmark.web_archive_snapshot_url) self.assertEqual("", bookmark.web_archive_snapshot_url)
def test_create_web_archive_snapshot_should_not_save_stale_bookmark_data(self): def test_create_web_archive_snapshot_should_not_save_stale_bookmark_data(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
@ -166,48 +217,66 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
# update bookmark during API call to check that saving # update bookmark during API call to check that saving
# the snapshot does not overwrite updated bookmark data # the snapshot does not overwrite updated bookmark data
def mock_save_impl(): def mock_save_impl():
bookmark.title = 'Updated title' bookmark.title = "Updated title"
bookmark.save() bookmark.save()
mock_save_api.save.side_effect = mock_save_impl mock_save_api.save.side_effect = mock_save_impl
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api): with mock.patch.object(
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False) waybackpy, "WaybackMachineSaveAPI", return_value=mock_save_api
):
tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, False
)
self.run_pending_task(tasks._create_web_archive_snapshot_task) self.run_pending_task(tasks._create_web_archive_snapshot_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual(bookmark.title, 'Updated title') self.assertEqual(bookmark.title, "Updated title")
self.assertEqual('https://example.com/created_snapshot', bookmark.web_archive_snapshot_url) self.assertEqual(
"https://example.com/created_snapshot",
bookmark.web_archive_snapshot_url,
)
def test_load_web_archive_snapshot_should_update_snapshot_url(self): def test_load_web_archive_snapshot_should_update_snapshot_url(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
mock_cdx_api = create_cdx_server_api_mock() mock_cdx_api = create_cdx_server_api_mock()
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', with mock.patch.object(
return_value=mock_cdx_api): bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks._load_web_archive_snapshot_task(bookmark.id) tasks._load_web_archive_snapshot_task(bookmark.id)
self.run_pending_task(tasks._load_web_archive_snapshot_task) self.run_pending_task(tasks._load_web_archive_snapshot_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
mock_cdx_api.newest.assert_called_once() mock_cdx_api.newest.assert_called_once()
self.assertEqual('https://example.com/newest_snapshot', bookmark.web_archive_snapshot_url) self.assertEqual(
"https://example.com/newest_snapshot", bookmark.web_archive_snapshot_url
)
def test_load_web_archive_snapshot_should_handle_missing_bookmark_id(self): def test_load_web_archive_snapshot_should_handle_missing_bookmark_id(self):
mock_cdx_api = create_cdx_server_api_mock() mock_cdx_api = create_cdx_server_api_mock()
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', with mock.patch.object(
return_value=mock_cdx_api): bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks._load_web_archive_snapshot_task(123) tasks._load_web_archive_snapshot_task(123)
self.run_pending_task(tasks._load_web_archive_snapshot_task) self.run_pending_task(tasks._load_web_archive_snapshot_task)
mock_cdx_api.newest.assert_not_called() mock_cdx_api.newest.assert_not_called()
def test_load_web_archive_snapshot_should_skip_if_snapshot_exists(self): def test_load_web_archive_snapshot_should_skip_if_snapshot_exists(self):
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com') bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com")
mock_cdx_api = create_cdx_server_api_mock() mock_cdx_api = create_cdx_server_api_mock()
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', with mock.patch.object(
return_value=mock_cdx_api): bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks._load_web_archive_snapshot_task(bookmark.id) tasks._load_web_archive_snapshot_task(bookmark.id)
self.run_pending_task(tasks._load_web_archive_snapshot_task) self.run_pending_task(tasks._load_web_archive_snapshot_task)
@ -217,23 +286,29 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
mock_cdx_api = create_cdx_server_api_mock(archive_url=None) mock_cdx_api = create_cdx_server_api_mock(archive_url=None)
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', with mock.patch.object(
return_value=mock_cdx_api): bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks._load_web_archive_snapshot_task(bookmark.id) tasks._load_web_archive_snapshot_task(bookmark.id)
self.run_pending_task(tasks._load_web_archive_snapshot_task) self.run_pending_task(tasks._load_web_archive_snapshot_task)
self.assertEqual('', bookmark.web_archive_snapshot_url) self.assertEqual("", bookmark.web_archive_snapshot_url)
def test_load_web_archive_snapshot_should_handle_wayback_errors(self): def test_load_web_archive_snapshot_should_handle_wayback_errors(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
mock_cdx_api = create_cdx_server_api_mock(fail_loading_snapshot=True) mock_cdx_api = create_cdx_server_api_mock(fail_loading_snapshot=True)
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', with mock.patch.object(
return_value=mock_cdx_api): bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks._load_web_archive_snapshot_task(bookmark.id) tasks._load_web_archive_snapshot_task(bookmark.id)
self.run_pending_task(tasks._load_web_archive_snapshot_task) self.run_pending_task(tasks._load_web_archive_snapshot_task)
self.assertEqual('', bookmark.web_archive_snapshot_url) self.assertEqual("", bookmark.web_archive_snapshot_url)
def test_load_web_archive_snapshot_should_not_save_stale_bookmark_data(self): def test_load_web_archive_snapshot_should_not_save_stale_bookmark_data(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
@ -242,45 +317,62 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
# update bookmark during API call to check that saving # update bookmark during API call to check that saving
# the snapshot does not overwrite updated bookmark data # the snapshot does not overwrite updated bookmark data
def mock_newest_impl(): def mock_newest_impl():
bookmark.title = 'Updated title' bookmark.title = "Updated title"
bookmark.save() bookmark.save()
return mock.DEFAULT return mock.DEFAULT
mock_cdx_api.newest.side_effect = mock_newest_impl mock_cdx_api.newest.side_effect = mock_newest_impl
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI', with mock.patch.object(
return_value=mock_cdx_api): bookmarks.services.wayback,
"CustomWaybackMachineCDXServerAPI",
return_value=mock_cdx_api,
):
tasks._load_web_archive_snapshot_task(bookmark.id) tasks._load_web_archive_snapshot_task(bookmark.id)
self.run_pending_task(tasks._load_web_archive_snapshot_task) self.run_pending_task(tasks._load_web_archive_snapshot_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual('Updated title', bookmark.title) self.assertEqual("Updated title", bookmark.title)
self.assertEqual('https://example.com/newest_snapshot', bookmark.web_archive_snapshot_url) self.assertEqual(
"https://example.com/newest_snapshot", bookmark.web_archive_snapshot_url
)
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True) @override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
def test_create_web_archive_snapshot_should_not_run_when_background_tasks_are_disabled(self): def test_create_web_archive_snapshot_should_not_run_when_background_tasks_are_disabled(
self,
):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False) tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, False
)
self.assertEqual(Task.objects.count(), 0) self.assertEqual(Task.objects.count(), 0)
def test_create_web_archive_snapshot_should_not_run_when_web_archive_integration_is_disabled(self): def test_create_web_archive_snapshot_should_not_run_when_web_archive_integration_is_disabled(
self.user.profile.web_archive_integration = UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED self,
):
self.user.profile.web_archive_integration = (
UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED
)
self.user.profile.save() self.user.profile.save()
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False) tasks.create_web_archive_snapshot(
self.get_or_create_test_user(), bookmark, False
)
self.assertEqual(Task.objects.count(), 0) self.assertEqual(Task.objects.count(), 0)
def test_schedule_bookmarks_without_snapshots_should_load_snapshot_for_all_bookmarks_without_snapshot(self): def test_schedule_bookmarks_without_snapshots_should_load_snapshot_for_all_bookmarks_without_snapshot(
self,
):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark(web_archive_snapshot_url='https://example.com') self.setup_bookmark(web_archive_snapshot_url="https://example.com")
self.setup_bookmark(web_archive_snapshot_url='https://example.com') self.setup_bookmark(web_archive_snapshot_url="https://example.com")
self.setup_bookmark(web_archive_snapshot_url='https://example.com') self.setup_bookmark(web_archive_snapshot_url="https://example.com")
tasks.schedule_bookmarks_without_snapshots(user) tasks.schedule_bookmarks_without_snapshots(user)
self.run_pending_task(tasks._schedule_bookmarks_without_snapshots_task) self.run_pending_task(tasks._schedule_bookmarks_without_snapshots_task)
@ -289,11 +381,18 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(task_list.count(), 3) self.assertEqual(task_list.count(), 3)
for task in task_list: for task in task_list:
self.assertEqual(task.task_name, 'bookmarks.services.tasks._load_web_archive_snapshot_task') self.assertEqual(
task.task_name,
"bookmarks.services.tasks._load_web_archive_snapshot_task",
)
def test_schedule_bookmarks_without_snapshots_should_only_update_user_owned_bookmarks(self): def test_schedule_bookmarks_without_snapshots_should_only_update_user_owned_bookmarks(
self,
):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
@ -308,13 +407,19 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(task_list.count(), 3) self.assertEqual(task_list.count(), 3)
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True) @override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
def test_schedule_bookmarks_without_snapshots_should_not_run_when_background_tasks_are_disabled(self): def test_schedule_bookmarks_without_snapshots_should_not_run_when_background_tasks_are_disabled(
self,
):
tasks.schedule_bookmarks_without_snapshots(self.user) tasks.schedule_bookmarks_without_snapshots(self.user)
self.assertEqual(Task.objects.count(), 0) self.assertEqual(Task.objects.count(), 0)
def test_schedule_bookmarks_without_snapshots_should_not_run_when_web_archive_integration_is_disabled(self): def test_schedule_bookmarks_without_snapshots_should_not_run_when_web_archive_integration_is_disabled(
self.user.profile.web_archive_integration = UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED self,
):
self.user.profile.web_archive_integration = (
UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED
)
self.user.profile.save() self.user.profile.save()
tasks.schedule_bookmarks_without_snapshots(self.user) tasks.schedule_bookmarks_without_snapshots(self.user)
@ -323,29 +428,35 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
def test_load_favicon_should_create_favicon_file(self): def test_load_favicon_should_create_favicon_file(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
with mock.patch('bookmarks.services.favicon_loader.load_favicon') as mock_load_favicon: with mock.patch(
mock_load_favicon.return_value = 'https_example_com.png' "bookmarks.services.favicon_loader.load_favicon"
) as mock_load_favicon:
mock_load_favicon.return_value = "https_example_com.png"
tasks.load_favicon(self.get_or_create_test_user(), bookmark) tasks.load_favicon(self.get_or_create_test_user(), bookmark)
self.run_pending_task(tasks._load_favicon_task) self.run_pending_task(tasks._load_favicon_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual(bookmark.favicon_file, 'https_example_com.png') self.assertEqual(bookmark.favicon_file, "https_example_com.png")
def test_load_favicon_should_update_favicon_file(self): def test_load_favicon_should_update_favicon_file(self):
bookmark = self.setup_bookmark(favicon_file='https_example_com.png') bookmark = self.setup_bookmark(favicon_file="https_example_com.png")
with mock.patch('bookmarks.services.favicon_loader.load_favicon') as mock_load_favicon: with mock.patch(
mock_load_favicon.return_value = 'https_example_updated_com.png' "bookmarks.services.favicon_loader.load_favicon"
) as mock_load_favicon:
mock_load_favicon.return_value = "https_example_updated_com.png"
tasks.load_favicon(self.get_or_create_test_user(), bookmark) tasks.load_favicon(self.get_or_create_test_user(), bookmark)
self.run_pending_task(tasks._load_favicon_task) self.run_pending_task(tasks._load_favicon_task)
mock_load_favicon.assert_called() mock_load_favicon.assert_called()
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual(bookmark.favicon_file, 'https_example_updated_com.png') self.assertEqual(bookmark.favicon_file, "https_example_updated_com.png")
def test_load_favicon_should_handle_missing_bookmark(self): def test_load_favicon_should_handle_missing_bookmark(self):
with mock.patch('bookmarks.services.favicon_loader.load_favicon') as mock_load_favicon: with mock.patch(
"bookmarks.services.favicon_loader.load_favicon"
) as mock_load_favicon:
tasks._load_favicon_task(123) tasks._load_favicon_task(123)
self.run_pending_task(tasks._load_favicon_task) self.run_pending_task(tasks._load_favicon_task)
@ -357,19 +468,21 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
# update bookmark during API call to check that saving # update bookmark during API call to check that saving
# the favicon does not overwrite updated bookmark data # the favicon does not overwrite updated bookmark data
def mock_load_favicon_impl(url): def mock_load_favicon_impl(url):
bookmark.title = 'Updated title' bookmark.title = "Updated title"
bookmark.save() bookmark.save()
return 'https_example_com.png' return "https_example_com.png"
with mock.patch('bookmarks.services.favicon_loader.load_favicon') as mock_load_favicon: with mock.patch(
"bookmarks.services.favicon_loader.load_favicon"
) as mock_load_favicon:
mock_load_favicon.side_effect = mock_load_favicon_impl mock_load_favicon.side_effect = mock_load_favicon_impl
tasks.load_favicon(self.get_or_create_test_user(), bookmark) tasks.load_favicon(self.get_or_create_test_user(), bookmark)
self.run_pending_task(tasks._load_favicon_task) self.run_pending_task(tasks._load_favicon_task)
bookmark.refresh_from_db() bookmark.refresh_from_db()
self.assertEqual(bookmark.title, 'Updated title') self.assertEqual(bookmark.title, "Updated title")
self.assertEqual(bookmark.favicon_file, 'https_example_com.png') self.assertEqual(bookmark.favicon_file, "https_example_com.png")
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True) @override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
def test_load_favicon_should_not_run_when_background_tasks_are_disabled(self): def test_load_favicon_should_not_run_when_background_tasks_are_disabled(self):
@ -387,14 +500,16 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(Task.objects.count(), 0) self.assertEqual(Task.objects.count(), 0)
def test_schedule_bookmarks_without_favicons_should_load_favicon_for_all_bookmarks_without_favicon(self): def test_schedule_bookmarks_without_favicons_should_load_favicon_for_all_bookmarks_without_favicon(
self,
):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark(favicon_file='https_example_com.png') self.setup_bookmark(favicon_file="https_example_com.png")
self.setup_bookmark(favicon_file='https_example_com.png') self.setup_bookmark(favicon_file="https_example_com.png")
self.setup_bookmark(favicon_file='https_example_com.png') self.setup_bookmark(favicon_file="https_example_com.png")
tasks.schedule_bookmarks_without_favicons(user) tasks.schedule_bookmarks_without_favicons(user)
self.run_pending_task(tasks._schedule_bookmarks_without_favicons_task) self.run_pending_task(tasks._schedule_bookmarks_without_favicons_task)
@ -403,11 +518,17 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(task_list.count(), 3) self.assertEqual(task_list.count(), 3)
for task in task_list: for task in task_list:
self.assertEqual(task.task_name, 'bookmarks.services.tasks._load_favicon_task') self.assertEqual(
task.task_name, "bookmarks.services.tasks._load_favicon_task"
)
def test_schedule_bookmarks_without_favicons_should_only_update_user_owned_bookmarks(self): def test_schedule_bookmarks_without_favicons_should_only_update_user_owned_bookmarks(
self,
):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
@ -422,13 +543,17 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(task_list.count(), 3) self.assertEqual(task_list.count(), 3)
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True) @override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
def test_schedule_bookmarks_without_favicons_should_not_run_when_background_tasks_are_disabled(self): def test_schedule_bookmarks_without_favicons_should_not_run_when_background_tasks_are_disabled(
self,
):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
tasks.schedule_bookmarks_without_favicons(self.get_or_create_test_user()) tasks.schedule_bookmarks_without_favicons(self.get_or_create_test_user())
self.assertEqual(Task.objects.count(), 0) self.assertEqual(Task.objects.count(), 0)
def test_schedule_bookmarks_without_favicons_should_not_run_when_favicon_feature_is_disabled(self): def test_schedule_bookmarks_without_favicons_should_not_run_when_favicon_feature_is_disabled(
self,
):
self.user.profile.enable_favicons = False self.user.profile.enable_favicons = False
self.user.profile.save() self.user.profile.save()
@ -442,9 +567,9 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark(favicon_file='https_example_com.png') self.setup_bookmark(favicon_file="https_example_com.png")
self.setup_bookmark(favicon_file='https_example_com.png') self.setup_bookmark(favicon_file="https_example_com.png")
self.setup_bookmark(favicon_file='https_example_com.png') self.setup_bookmark(favicon_file="https_example_com.png")
tasks.schedule_refresh_favicons(user) tasks.schedule_refresh_favicons(user)
self.run_pending_task(tasks._schedule_refresh_favicons_task) self.run_pending_task(tasks._schedule_refresh_favicons_task)
@ -453,11 +578,15 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(task_list.count(), 6) self.assertEqual(task_list.count(), 6)
for task in task_list: for task in task_list:
self.assertEqual(task.task_name, 'bookmarks.services.tasks._load_favicon_task') self.assertEqual(
task.task_name, "bookmarks.services.tasks._load_favicon_task"
)
def test_schedule_refresh_favicons_should_only_update_user_owned_bookmarks(self): def test_schedule_refresh_favicons_should_only_update_user_owned_bookmarks(self):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
@ -472,7 +601,9 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(task_list.count(), 3) self.assertEqual(task_list.count(), 3)
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True) @override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
def test_schedule_refresh_favicons_should_not_run_when_background_tasks_are_disabled(self): def test_schedule_refresh_favicons_should_not_run_when_background_tasks_are_disabled(
self,
):
self.setup_bookmark() self.setup_bookmark()
tasks.schedule_refresh_favicons(self.get_or_create_test_user()) tasks.schedule_refresh_favicons(self.get_or_create_test_user())
@ -485,7 +616,9 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
self.assertEqual(Task.objects.count(), 0) self.assertEqual(Task.objects.count(), 0)
def test_schedule_refresh_favicons_should_not_run_when_favicon_feature_is_disabled(self): def test_schedule_refresh_favicons_should_not_run_when_favicon_feature_is_disabled(
self,
):
self.user.profile.enable_favicons = False self.user.profile.enable_favicons = False
self.user.profile.save() self.user.profile.save()

View file

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

View file

@ -9,25 +9,28 @@ from bookmarks.management.commands.create_initial_superuser import Command
class TestCreateInitialSuperuserCommand(TestCase): class TestCreateInitialSuperuserCommand(TestCase):
@mock.patch.dict(os.environ, {'LD_SUPERUSER_NAME': 'john', 'LD_SUPERUSER_PASSWORD': 'password123'}) @mock.patch.dict(
os.environ,
{"LD_SUPERUSER_NAME": "john", "LD_SUPERUSER_PASSWORD": "password123"},
)
def test_create_with_password(self): def test_create_with_password(self):
Command().handle() Command().handle()
self.assertEqual(1, User.objects.count()) self.assertEqual(1, User.objects.count())
user = User.objects.first() user = User.objects.first()
self.assertEqual('john', user.username) self.assertEqual("john", user.username)
self.assertTrue(user.has_usable_password()) self.assertTrue(user.has_usable_password())
self.assertTrue(user.check_password('password123')) self.assertTrue(user.check_password("password123"))
@mock.patch.dict(os.environ, {'LD_SUPERUSER_NAME': 'john'}) @mock.patch.dict(os.environ, {"LD_SUPERUSER_NAME": "john"})
def test_create_without_password(self): def test_create_without_password(self):
Command().handle() Command().handle()
self.assertEqual(1, User.objects.count()) self.assertEqual(1, User.objects.count())
user = User.objects.first() user = User.objects.first()
self.assertEqual('john', user.username) self.assertEqual("john", user.username)
self.assertFalse(user.has_usable_password()) self.assertFalse(user.has_usable_password())
def test_create_without_options(self): def test_create_without_options(self):
@ -35,11 +38,13 @@ class TestCreateInitialSuperuserCommand(TestCase):
self.assertEqual(0, User.objects.count()) self.assertEqual(0, User.objects.count())
@mock.patch.dict(os.environ, {'LD_SUPERUSER_NAME': 'john', 'LD_SUPERUSER_PASSWORD': 'password123'}) @mock.patch.dict(
os.environ,
{"LD_SUPERUSER_NAME": "john", "LD_SUPERUSER_PASSWORD": "password123"},
)
def test_create_multiple_times(self): def test_create_multiple_times(self):
Command().handle() Command().handle()
Command().handle() Command().handle()
Command().handle() Command().handle()
self.assertEqual(1, User.objects.count()) self.assertEqual(1, User.objects.count())

View file

@ -11,60 +11,93 @@ class ExporterTestCase(TestCase, BookmarkFactoryMixin):
timestamp = int(added.timestamp()) timestamp = int(added.timestamp())
bookmarks = [ bookmarks = [
self.setup_bookmark(url='https://example.com/1', title='Title 1', added=added, self.setup_bookmark(
description='Example description'), url="https://example.com/1",
self.setup_bookmark(url='https://example.com/2', title='Title 2', added=added, title="Title 1",
tags=[self.setup_tag(name='tag1'), self.setup_tag(name='tag2'), added=added,
self.setup_tag(name='tag3')]), description="Example description",
self.setup_bookmark(url='https://example.com/3', title='Title 3', added=added, unread=True), ),
self.setup_bookmark(url='https://example.com/4', title='Title 4', added=added, shared=True), self.setup_bookmark(
self.setup_bookmark(url='https://example.com/5', title='Title 5', added=added, shared=True, url="https://example.com/2",
description='Example description', notes='Example notes'), title="Title 2",
self.setup_bookmark(url='https://example.com/6', title='Title 6', added=added, shared=True, added=added,
notes='Example notes'), tags=[
self.setup_bookmark(url='https://example.com/7', title='Title 7', added=added, is_archived=True), self.setup_tag(name="tag1"),
self.setup_bookmark(url='https://example.com/8', title='Title 8', added=added, self.setup_tag(name="tag2"),
tags=[self.setup_tag(name='tag4'), self.setup_tag(name='tag5')], is_archived=True), self.setup_tag(name="tag3"),
],
),
self.setup_bookmark(
url="https://example.com/3", title="Title 3", added=added, unread=True
),
self.setup_bookmark(
url="https://example.com/4", title="Title 4", added=added, shared=True
),
self.setup_bookmark(
url="https://example.com/5",
title="Title 5",
added=added,
shared=True,
description="Example description",
notes="Example notes",
),
self.setup_bookmark(
url="https://example.com/6",
title="Title 6",
added=added,
shared=True,
notes="Example notes",
),
self.setup_bookmark(
url="https://example.com/7",
title="Title 7",
added=added,
is_archived=True,
),
self.setup_bookmark(
url="https://example.com/8",
title="Title 8",
added=added,
tags=[self.setup_tag(name="tag4"), self.setup_tag(name="tag5")],
is_archived=True,
),
] ]
html = exporter.export_netscape_html(bookmarks) html = exporter.export_netscape_html(bookmarks)
lines = [ lines = [
f'<DT><A HREF="https://example.com/1" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="">Title 1</A>', f'<DT><A HREF="https://example.com/1" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="">Title 1</A>',
'<DD>Example description', "<DD>Example description",
f'<DT><A HREF="https://example.com/2" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag1,tag2,tag3">Title 2</A>', f'<DT><A HREF="https://example.com/2" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag1,tag2,tag3">Title 2</A>',
f'<DT><A HREF="https://example.com/3" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="1" TAGS="">Title 3</A>', f'<DT><A HREF="https://example.com/3" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="1" TAGS="">Title 3</A>',
f'<DT><A HREF="https://example.com/4" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 4</A>', f'<DT><A HREF="https://example.com/4" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 4</A>',
f'<DT><A HREF="https://example.com/5" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 5</A>', f'<DT><A HREF="https://example.com/5" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 5</A>',
'<DD>Example description[linkding-notes]Example notes[/linkding-notes]', "<DD>Example description[linkding-notes]Example notes[/linkding-notes]",
f'<DT><A HREF="https://example.com/6" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>', f'<DT><A HREF="https://example.com/6" ADD_DATE="{timestamp}" PRIVATE="0" TOREAD="0" TAGS="">Title 6</A>',
'<DD>[linkding-notes]Example notes[/linkding-notes]', "<DD>[linkding-notes]Example notes[/linkding-notes]",
f'<DT><A HREF="https://example.com/7" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>', f'<DT><A HREF="https://example.com/7" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="linkding:archived">Title 7</A>',
f'<DT><A HREF="https://example.com/8" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>', f'<DT><A HREF="https://example.com/8" ADD_DATE="{timestamp}" PRIVATE="1" TOREAD="0" TAGS="tag4,tag5,linkding:archived">Title 8</A>',
] ]
self.assertIn('\n\r'.join(lines), html) self.assertIn("\n\r".join(lines), html)
def test_escape_html(self): def test_escape_html(self):
bookmark = self.setup_bookmark( bookmark = self.setup_bookmark(
title='<style>: The Style Information element', title="<style>: The Style Information element",
description='The <style> HTML element contains style information for a document, or part of a document.', description="The <style> HTML element contains style information for a document, or part of a document.",
notes='Interesting notes about the <style> HTML element.', notes="Interesting notes about the <style> HTML element.",
) )
html = exporter.export_netscape_html([bookmark]) html = exporter.export_netscape_html([bookmark])
self.assertIn('&lt;style&gt;: The Style Information element', html) self.assertIn("&lt;style&gt;: The Style Information element", html)
self.assertIn( self.assertIn(
'The &lt;style&gt; HTML element contains style information for a document, or part of a document.', "The &lt;style&gt; HTML element contains style information for a document, or part of a document.",
html html,
)
self.assertIn(
'Interesting notes about the &lt;style&gt; HTML element.',
html
) )
self.assertIn("Interesting notes about the &lt;style&gt; HTML element.", html)
def test_handle_empty_values(self): def test_handle_empty_values(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
bookmark.title = '' bookmark.title = ""
bookmark.description = '' bookmark.description = ""
bookmark.website_title = None bookmark.website_title = None
bookmark.website_description = None bookmark.website_description = None
bookmark.save() bookmark.save()

View file

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

View file

@ -9,13 +9,13 @@ from django.test import TestCase, override_settings
from bookmarks.services import favicon_loader from bookmarks.services import favicon_loader
mock_icon_data = b'mock_icon' mock_icon_data = b"mock_icon"
class MockStreamingResponse: class MockStreamingResponse:
def __init__(self, data=mock_icon_data, content_type='image/png'): def __init__(self, data=mock_icon_data, content_type="image/png"):
self.chunks = [data] self.chunks = [data]
self.headers = {'Content-Type': content_type} self.headers = {"Content-Type": content_type}
def iter_content(self, **kwargs): def iter_content(self, **kwargs):
return self.chunks return self.chunks
@ -32,7 +32,7 @@ class FaviconLoaderTestCase(TestCase):
self.ensure_favicon_folder() self.ensure_favicon_folder()
self.clear_favicon_folder() self.clear_favicon_folder()
def create_mock_response(self, icon_data=mock_icon_data, content_type='image/png'): def create_mock_response(self, icon_data=mock_icon_data, content_type="image/png"):
mock_response = mock.Mock() mock_response = mock.Mock()
mock_response.raw = io.BytesIO(icon_data) mock_response.raw = io.BytesIO(icon_data)
return MockStreamingResponse(icon_data, content_type) return MockStreamingResponse(icon_data, content_type)
@ -59,18 +59,20 @@ class FaviconLoaderTestCase(TestCase):
return len(files) return len(files)
def test_load_favicon(self): def test_load_favicon(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response() mock_get.return_value = self.create_mock_response()
favicon_loader.load_favicon('https://example.com') favicon_loader.load_favicon("https://example.com")
# should create icon file # should create icon file
self.assertTrue(self.icon_exists('https_example_com.png')) self.assertTrue(self.icon_exists("https_example_com.png"))
# should store image data # should store image data
self.assertEqual(mock_icon_data, self.get_icon_data('https_example_com.png')) self.assertEqual(
mock_icon_data, self.get_icon_data("https_example_com.png")
)
def test_load_favicon_creates_folder_if_not_exists(self): def test_load_favicon_creates_folder_if_not_exists(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response() mock_get.return_value = self.create_mock_response()
folder = Path(settings.LD_FAVICON_FOLDER) folder = Path(settings.LD_FAVICON_FOLDER)
@ -78,99 +80,109 @@ class FaviconLoaderTestCase(TestCase):
self.assertFalse(folder.exists()) self.assertFalse(folder.exists())
favicon_loader.load_favicon('https://example.com') favicon_loader.load_favicon("https://example.com")
self.assertTrue(folder.exists()) self.assertTrue(folder.exists())
def test_load_favicon_creates_single_icon_for_same_base_url(self): def test_load_favicon_creates_single_icon_for_same_base_url(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response() mock_get.return_value = self.create_mock_response()
favicon_loader.load_favicon('https://example.com') favicon_loader.load_favicon("https://example.com")
favicon_loader.load_favicon('https://example.com?foo=bar') favicon_loader.load_favicon("https://example.com?foo=bar")
favicon_loader.load_favicon('https://example.com/foo') favicon_loader.load_favicon("https://example.com/foo")
self.assertEqual(1, self.count_icons()) self.assertEqual(1, self.count_icons())
self.assertTrue(self.icon_exists('https_example_com.png')) self.assertTrue(self.icon_exists("https_example_com.png"))
def test_load_favicon_creates_multiple_icons_for_different_base_url(self): def test_load_favicon_creates_multiple_icons_for_different_base_url(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response() mock_get.return_value = self.create_mock_response()
favicon_loader.load_favicon('https://example.com') favicon_loader.load_favicon("https://example.com")
favicon_loader.load_favicon('https://sub.example.com') favicon_loader.load_favicon("https://sub.example.com")
favicon_loader.load_favicon('https://other-domain.com') favicon_loader.load_favicon("https://other-domain.com")
self.assertEqual(3, self.count_icons()) self.assertEqual(3, self.count_icons())
self.assertTrue(self.icon_exists('https_example_com.png')) self.assertTrue(self.icon_exists("https_example_com.png"))
self.assertTrue(self.icon_exists('https_sub_example_com.png')) self.assertTrue(self.icon_exists("https_sub_example_com.png"))
self.assertTrue(self.icon_exists('https_other_domain_com.png')) self.assertTrue(self.icon_exists("https_other_domain_com.png"))
def test_load_favicon_caches_icons(self): def test_load_favicon_caches_icons(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response() mock_get.return_value = self.create_mock_response()
favicon_file = favicon_loader.load_favicon('https://example.com') favicon_file = favicon_loader.load_favicon("https://example.com")
mock_get.assert_called() mock_get.assert_called()
self.assertEqual(favicon_file, 'https_example_com.png') self.assertEqual(favicon_file, "https_example_com.png")
mock_get.reset_mock() mock_get.reset_mock()
updated_favicon_file = favicon_loader.load_favicon('https://example.com') updated_favicon_file = favicon_loader.load_favicon("https://example.com")
mock_get.assert_not_called() mock_get.assert_not_called()
self.assertEqual(favicon_file, updated_favicon_file) self.assertEqual(favicon_file, updated_favicon_file)
def test_load_favicon_updates_stale_icon(self): def test_load_favicon_updates_stale_icon(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response() mock_get.return_value = self.create_mock_response()
favicon_loader.load_favicon('https://example.com') favicon_loader.load_favicon("https://example.com")
icon_path = self.get_icon_path('https_example_com.png') icon_path = self.get_icon_path("https_example_com.png")
updated_mock_icon_data = b'updated_mock_icon' updated_mock_icon_data = b"updated_mock_icon"
mock_get.return_value = self.create_mock_response(icon_data=updated_mock_icon_data) mock_get.return_value = self.create_mock_response(
icon_data=updated_mock_icon_data
)
mock_get.reset_mock() mock_get.reset_mock()
# change icon modification date so it is not stale yet # change icon modification date so it is not stale yet
nearly_one_day_ago = time.time() - 60 * 60 * 23 nearly_one_day_ago = time.time() - 60 * 60 * 23
os.utime(icon_path.absolute(), (nearly_one_day_ago, nearly_one_day_ago)) os.utime(icon_path.absolute(), (nearly_one_day_ago, nearly_one_day_ago))
favicon_loader.load_favicon('https://example.com') favicon_loader.load_favicon("https://example.com")
mock_get.assert_not_called() mock_get.assert_not_called()
# change icon modification date so it is considered stale # change icon modification date so it is considered stale
one_day_ago = time.time() - 60 * 60 * 24 one_day_ago = time.time() - 60 * 60 * 24
os.utime(icon_path.absolute(), (one_day_ago, one_day_ago)) os.utime(icon_path.absolute(), (one_day_ago, one_day_ago))
favicon_loader.load_favicon('https://example.com') favicon_loader.load_favicon("https://example.com")
mock_get.assert_called() mock_get.assert_called()
self.assertEqual(updated_mock_icon_data, self.get_icon_data('https_example_com.png')) self.assertEqual(
updated_mock_icon_data, self.get_icon_data("https_example_com.png")
)
@override_settings(LD_FAVICON_PROVIDER='https://custom.icons.com/?url={url}') @override_settings(LD_FAVICON_PROVIDER="https://custom.icons.com/?url={url}")
def test_custom_provider_with_url_param(self): def test_custom_provider_with_url_param(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response() mock_get.return_value = self.create_mock_response()
favicon_loader.load_favicon('https://example.com/foo?bar=baz') favicon_loader.load_favicon("https://example.com/foo?bar=baz")
mock_get.assert_called_with('https://custom.icons.com/?url=https://example.com', stream=True) mock_get.assert_called_with(
"https://custom.icons.com/?url=https://example.com", stream=True
)
@override_settings(LD_FAVICON_PROVIDER='https://custom.icons.com/?url={domain}') @override_settings(LD_FAVICON_PROVIDER="https://custom.icons.com/?url={domain}")
def test_custom_provider_with_domain_param(self): def test_custom_provider_with_domain_param(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response() mock_get.return_value = self.create_mock_response()
favicon_loader.load_favicon('https://example.com/foo?bar=baz') favicon_loader.load_favicon("https://example.com/foo?bar=baz")
mock_get.assert_called_with('https://custom.icons.com/?url=example.com', stream=True) mock_get.assert_called_with(
"https://custom.icons.com/?url=example.com", stream=True
)
def test_guess_file_extension(self): def test_guess_file_extension(self):
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response(content_type='image/png') mock_get.return_value = self.create_mock_response(content_type="image/png")
favicon_loader.load_favicon('https://example.com') favicon_loader.load_favicon("https://example.com")
self.assertTrue(self.icon_exists('https_example_com.png')) self.assertTrue(self.icon_exists("https_example_com.png"))
self.clear_favicon_folder() self.clear_favicon_folder()
self.ensure_favicon_folder() self.ensure_favicon_folder()
with mock.patch('requests.get') as mock_get: with mock.patch("requests.get") as mock_get:
mock_get.return_value = self.create_mock_response(content_type='image/x-icon') mock_get.return_value = self.create_mock_response(
favicon_loader.load_favicon('https://example.com') content_type="image/x-icon"
)
favicon_loader.load_favicon("https://example.com")
self.assertTrue(self.icon_exists('https_example_com.ico')) self.assertTrue(self.icon_exists("https_example_com.ico"))

View file

@ -10,7 +10,6 @@ from bookmarks.models import FeedToken, User
from bookmarks.feeds import sanitize from bookmarks.feeds import sanitize
def rfc2822_date(date): def rfc2822_date(date):
if not isinstance(date, datetime.datetime): if not isinstance(date, datetime.datetime):
date = datetime.datetime.combine(date, datetime.time()) date = datetime.datetime.combine(date, datetime.time())
@ -25,43 +24,49 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.token = FeedToken.objects.get_or_create(user=user)[0] self.token = FeedToken.objects.get_or_create(user=user)[0]
def test_all_returns_404_for_unknown_feed_token(self): 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("bookmarks:feeds.all", args=["foo"]))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_all_metadata(self): def test_all_metadata(self):
feed_url = reverse('bookmarks:feeds.all', args=[self.token.key]) feed_url = reverse("bookmarks:feeds.all", args=[self.token.key])
response = self.client.get(feed_url) response = self.client.get(feed_url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<title>All bookmarks</title>') self.assertContains(response, "<title>All bookmarks</title>")
self.assertContains(response, '<description>All bookmarks</description>') self.assertContains(response, "<description>All bookmarks</description>")
self.assertContains(response, f'<link>http://testserver{feed_url}</link>') self.assertContains(response, f"<link>http://testserver{feed_url}</link>")
self.assertContains(response, f'<atom:link href="http://testserver{feed_url}" rel="self"/>') self.assertContains(
response, f'<atom:link href="http://testserver{feed_url}" rel="self"/>'
)
def test_all_returns_all_unarchived_bookmarks(self): def test_all_returns_all_unarchived_bookmarks(self):
bookmarks = [ bookmarks = [
self.setup_bookmark(description='test description'), self.setup_bookmark(description="test description"),
self.setup_bookmark(website_description='test website description'), self.setup_bookmark(website_description="test website description"),
self.setup_bookmark(unread=True, description='test description'), self.setup_bookmark(unread=True, description="test description"),
] ]
self.setup_bookmark(is_archived=True) self.setup_bookmark(is_archived=True)
self.setup_bookmark(is_archived=True) self.setup_bookmark(is_archived=True)
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("bookmarks:feeds.all", args=[self.token.key])
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=len(bookmarks)) self.assertContains(response, "<item>", count=len(bookmarks))
for bookmark in bookmarks: for bookmark in bookmarks:
expected_item = '<item>' \ expected_item = (
f'<title>{bookmark.resolved_title}</title>' \ "<item>"
f'<link>{bookmark.url}</link>' \ f"<title>{bookmark.resolved_title}</title>"
f'<description>{bookmark.resolved_description}</description>' \ f"<link>{bookmark.url}</link>"
f'<pubDate>{rfc2822_date(bookmark.date_added)}</pubDate>' \ f"<description>{bookmark.resolved_description}</description>"
f'<guid>{bookmark.url}</guid>' \ f"<pubDate>{rfc2822_date(bookmark.date_added)}</pubDate>"
'</item>' f"<guid>{bookmark.url}</guid>"
"</item>"
)
self.assertContains(response, expected_item, count=1) self.assertContains(response, expected_item, count=1)
def test_all_with_query(self): def test_all_with_query(self):
@ -74,63 +79,75 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark() self.setup_bookmark()
self.setup_bookmark() self.setup_bookmark()
feed_url = reverse('bookmarks:feeds.all', args=[self.token.key]) feed_url = reverse("bookmarks:feeds.all", args=[self.token.key])
url = feed_url + f'?q={bookmark1.title}' url = feed_url + f"?q={bookmark1.title}"
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=1) self.assertContains(response, "<item>", count=1)
self.assertContains(response, f'<guid>{bookmark1.url}</guid>', count=1) self.assertContains(response, f"<guid>{bookmark1.url}</guid>", count=1)
url = feed_url + '?q=' + urllib.parse.quote('#' + tag1.name) url = feed_url + "?q=" + urllib.parse.quote("#" + tag1.name)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=2) self.assertContains(response, "<item>", count=2)
self.assertContains(response, f'<guid>{bookmark2.url}</guid>', count=1) self.assertContains(response, f"<guid>{bookmark2.url}</guid>", count=1)
self.assertContains(response, f'<guid>{bookmark3.url}</guid>', count=1) self.assertContains(response, f"<guid>{bookmark3.url}</guid>", count=1)
url = feed_url + '?q=' + urllib.parse.quote(f'#{tag1.name} {bookmark2.title}') url = feed_url + "?q=" + urllib.parse.quote(f"#{tag1.name} {bookmark2.title}")
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=1) self.assertContains(response, "<item>", count=1)
self.assertContains(response, f'<guid>{bookmark2.url}</guid>', count=1) self.assertContains(response, f"<guid>{bookmark2.url}</guid>", count=1)
def test_all_returns_only_user_owned_bookmarks(self): def test_all_returns_only_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
self.setup_bookmark(unread=True, user=other_user) self.setup_bookmark(unread=True, user=other_user)
self.setup_bookmark(unread=True, user=other_user) self.setup_bookmark(unread=True, user=other_user)
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("bookmarks:feeds.all", args=[self.token.key])
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=0) self.assertContains(response, "<item>", count=0)
def test_strip_control_characters(self): def test_strip_control_characters(self):
self.setup_bookmark(title='test\n\r\t\0\x08title', description='test\n\r\t\0\x08description') self.setup_bookmark(
response = self.client.get(reverse('bookmarks:feeds.all', args=[self.token.key])) 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])
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=1) self.assertContains(response, "<item>", count=1)
self.assertContains(response, f'<title>test\n\r\ttitle</title>', count=1) self.assertContains(response, f"<title>test\n\r\ttitle</title>", count=1)
self.assertContains(response, f'<description>test\n\r\tdescription</description>', count=1) self.assertContains(
response, f"<description>test\n\r\tdescription</description>", count=1
)
def test_sanitize_with_none_text(self): def test_sanitize_with_none_text(self):
self.assertEqual('', sanitize(None)) self.assertEqual("", sanitize(None))
def test_unread_returns_404_for_unknown_feed_token(self): 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("bookmarks:feeds.unread", args=["foo"]))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_unread_metadata(self): def test_unread_metadata(self):
feed_url = reverse('bookmarks:feeds.unread', args=[self.token.key]) feed_url = reverse("bookmarks:feeds.unread", args=[self.token.key])
response = self.client.get(feed_url) response = self.client.get(feed_url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<title>Unread bookmarks</title>') self.assertContains(response, "<title>Unread bookmarks</title>")
self.assertContains(response, '<description>All unread bookmarks</description>') self.assertContains(response, "<description>All unread bookmarks</description>")
self.assertContains(response, f'<link>http://testserver{feed_url}</link>') self.assertContains(response, f"<link>http://testserver{feed_url}</link>")
self.assertContains(response, f'<atom:link href="http://testserver{feed_url}" rel="self"/>') self.assertContains(
response, f'<atom:link href="http://testserver{feed_url}" rel="self"/>'
)
def test_unread_returns_unread_and_unarchived_bookmarks(self): def test_unread_returns_unread_and_unarchived_bookmarks(self):
self.setup_bookmark(unread=False) self.setup_bookmark(unread=False)
@ -141,24 +158,30 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(unread=False, is_archived=True) self.setup_bookmark(unread=False, is_archived=True)
unread_bookmarks = [ unread_bookmarks = [
self.setup_bookmark(unread=True, description='test description'), self.setup_bookmark(unread=True, description="test description"),
self.setup_bookmark(unread=True, website_description='test website description'), self.setup_bookmark(
self.setup_bookmark(unread=True, description='test description'), unread=True, website_description="test website description"
),
self.setup_bookmark(unread=True, description="test description"),
] ]
response = self.client.get(reverse('bookmarks:feeds.unread', args=[self.token.key])) response = self.client.get(
reverse("bookmarks:feeds.unread", args=[self.token.key])
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=len(unread_bookmarks)) self.assertContains(response, "<item>", count=len(unread_bookmarks))
for bookmark in unread_bookmarks: for bookmark in unread_bookmarks:
expected_item = '<item>' \ expected_item = (
f'<title>{bookmark.resolved_title}</title>' \ "<item>"
f'<link>{bookmark.url}</link>' \ f"<title>{bookmark.resolved_title}</title>"
f'<description>{bookmark.resolved_description}</description>' \ f"<link>{bookmark.url}</link>"
f'<pubDate>{rfc2822_date(bookmark.date_added)}</pubDate>' \ f"<description>{bookmark.resolved_description}</description>"
f'<guid>{bookmark.url}</guid>' \ f"<pubDate>{rfc2822_date(bookmark.date_added)}</pubDate>"
'</item>' f"<guid>{bookmark.url}</guid>"
"</item>"
)
self.assertContains(response, expected_item, count=1) self.assertContains(response, expected_item, count=1)
def test_unread_with_query(self): def test_unread_with_query(self):
@ -171,34 +194,38 @@ class FeedsTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(unread=True) self.setup_bookmark(unread=True)
self.setup_bookmark(unread=True) self.setup_bookmark(unread=True)
feed_url = reverse('bookmarks:feeds.all', args=[self.token.key]) feed_url = reverse("bookmarks:feeds.all", args=[self.token.key])
url = feed_url + f'?q={bookmark1.title}' url = feed_url + f"?q={bookmark1.title}"
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=1) self.assertContains(response, "<item>", count=1)
self.assertContains(response, f'<guid>{bookmark1.url}</guid>', count=1) self.assertContains(response, f"<guid>{bookmark1.url}</guid>", count=1)
url = feed_url + '?q=' + urllib.parse.quote('#' + tag1.name) url = feed_url + "?q=" + urllib.parse.quote("#" + tag1.name)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=2) self.assertContains(response, "<item>", count=2)
self.assertContains(response, f'<guid>{bookmark2.url}</guid>', count=1) self.assertContains(response, f"<guid>{bookmark2.url}</guid>", count=1)
self.assertContains(response, f'<guid>{bookmark3.url}</guid>', count=1) self.assertContains(response, f"<guid>{bookmark3.url}</guid>", count=1)
url = feed_url + '?q=' + urllib.parse.quote(f'#{tag1.name} {bookmark2.title}') url = feed_url + "?q=" + urllib.parse.quote(f"#{tag1.name} {bookmark2.title}")
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=1) self.assertContains(response, "<item>", count=1)
self.assertContains(response, f'<guid>{bookmark2.url}</guid>', count=1) self.assertContains(response, f"<guid>{bookmark2.url}</guid>", count=1)
def test_unread_returns_only_user_owned_bookmarks(self): def test_unread_returns_only_user_owned_bookmarks(self):
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user(
"otheruser", "otheruser@example.com", "password123"
)
self.setup_bookmark(unread=True, user=other_user) self.setup_bookmark(unread=True, user=other_user)
self.setup_bookmark(unread=True, user=other_user) self.setup_bookmark(unread=True, user=other_user)
self.setup_bookmark(unread=True, user=other_user) self.setup_bookmark(unread=True, user=other_user)
response = self.client.get(reverse('bookmarks:feeds.unread', args=[self.token.key])) response = self.client.get(
reverse("bookmarks:feeds.unread", args=[self.token.key])
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, '<item>', count=0) self.assertContains(response, "<item>", count=0)

View file

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

View file

@ -14,23 +14,19 @@ class HealthViewTestCase(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response_body = response.json() response_body = response.json()
expected_body = { expected_body = {"version": app_version, "status": "healthy"}
'version': app_version,
'status': 'healthy'
}
self.assertDictEqual(response_body, expected_body) self.assertDictEqual(response_body, expected_body)
def test_health_unhealhty(self): def test_health_unhealhty(self):
with patch.object(connections['default'], 'ensure_connection') as mock_ensure_connection: with patch.object(
mock_ensure_connection.side_effect = Exception('Connection error') connections["default"], "ensure_connection"
) as mock_ensure_connection:
mock_ensure_connection.side_effect = Exception("Connection error")
response = self.client.get("/health") response = self.client.get("/health")
self.assertEqual(response.status_code, 500) self.assertEqual(response.status_code, 500)
response_body = response.json() response_body = response.json()
expected_body = { expected_body = {"version": app_version, "status": "unhealthy"}
'version': app_version,
'status': 'unhealthy'
}
self.assertDictEqual(response_body, expected_body) self.assertDictEqual(response_body, expected_body)

View file

@ -7,7 +7,12 @@ from django.utils import timezone
from bookmarks.models import Bookmark, Tag, parse_tag_string from bookmarks.models import Bookmark, Tag, parse_tag_string
from bookmarks.services import tasks from bookmarks.services import tasks
from bookmarks.services.importer import import_netscape_html, ImportOptions from bookmarks.services.importer import import_netscape_html, ImportOptions
from bookmarks.tests.helpers import BookmarkFactoryMixin, ImportTestMixin, BookmarkHtmlTag, disable_logging from bookmarks.tests.helpers import (
BookmarkFactoryMixin,
ImportTestMixin,
BookmarkHtmlTag,
disable_logging,
)
from bookmarks.utils import parse_timestamp from bookmarks.utils import parse_timestamp
@ -29,19 +34,40 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
# Check assigned tags # Check assigned tags
for tag_name in tag_names: for tag_name in tag_names:
tag = next( tag = next(
(tag for tag in bookmark.tags.all() if tag.name == tag_name), None) (tag for tag in bookmark.tags.all() if tag.name == tag_name), None
)
self.assertIsNotNone(tag) self.assertIsNotNone(tag)
def test_import(self): def test_import(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='Example title', description='Example description', BookmarkHtmlTag(
add_date='1', tags='example-tag'), href="https://example.com",
BookmarkHtmlTag(href='https://example.com/foo', title='Foo title', description='', title="Example title",
add_date='2', tags=''), description="Example description",
BookmarkHtmlTag(href='https://example.com/bar', title='Bar title', description='Bar description', add_date="1",
add_date='3', tags='bar-tag, other-tag'), tags="example-tag",
BookmarkHtmlTag(href='https://example.com/baz', title='Baz title', description='Baz description', ),
add_date='4', to_read=True), BookmarkHtmlTag(
href="https://example.com/foo",
title="Foo title",
description="",
add_date="2",
tags="",
),
BookmarkHtmlTag(
href="https://example.com/bar",
title="Bar title",
description="Bar description",
add_date="3",
tags="bar-tag, other-tag",
),
BookmarkHtmlTag(
href="https://example.com/baz",
title="Baz title",
description="Baz description",
add_date="4",
to_read=True,
),
] ]
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
result = import_netscape_html(import_html, self.get_or_create_test_user()) result = import_netscape_html(import_html, self.get_or_create_test_user())
@ -59,17 +85,41 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
def test_synchronize(self): def test_synchronize(self):
# Initial import # Initial import
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='Example title', description='Example description', BookmarkHtmlTag(
add_date='1', tags='example-tag'), href="https://example.com",
BookmarkHtmlTag(href='https://example.com/foo', title='Foo title', description='', title="Example title",
add_date='2', tags=''), description="Example description",
BookmarkHtmlTag(href='https://example.com/bar', title='Bar title', description='Bar description', add_date="1",
add_date='3', tags='bar-tag, other-tag'), tags="example-tag",
BookmarkHtmlTag(href='https://example.com/unread', title='Unread title', description='Unread description', ),
add_date='3', to_read=True), BookmarkHtmlTag(
BookmarkHtmlTag(href='https://example.com/private', title='Private title', href="https://example.com/foo",
description='Private description', title="Foo title",
add_date='4', private=True), description="",
add_date="2",
tags="",
),
BookmarkHtmlTag(
href="https://example.com/bar",
title="Bar title",
description="Bar description",
add_date="3",
tags="bar-tag, other-tag",
),
BookmarkHtmlTag(
href="https://example.com/unread",
title="Unread title",
description="Unread description",
add_date="3",
to_read=True,
),
BookmarkHtmlTag(
href="https://example.com/private",
title="Private title",
description="Private description",
add_date="4",
private=True,
),
] ]
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
import_netscape_html(import_html, self.get_or_create_test_user()) import_netscape_html(import_html, self.get_or_create_test_user())
@ -81,25 +131,51 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
# Change data, add some new data # Change data, add some new data
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='Updated Example title', BookmarkHtmlTag(
description='Updated Example description', add_date='111', tags='updated-example-tag'), href="https://example.com",
BookmarkHtmlTag(href='https://example.com/foo', title='Updated Foo title', title="Updated Example title",
description='Updated Foo description', description="Updated Example description",
add_date='222', tags='new-tag'), add_date="111",
BookmarkHtmlTag(href='https://example.com/bar', title='Updated Bar title', tags="updated-example-tag",
description='Updated Bar description', ),
add_date='333', tags='updated-bar-tag, updated-other-tag'), BookmarkHtmlTag(
BookmarkHtmlTag(href='https://example.com/unread', title='Unread title', description='Unread description', href="https://example.com/foo",
add_date='3', to_read=False), title="Updated Foo title",
BookmarkHtmlTag(href='https://example.com/private', title='Private title', description="Updated Foo description",
description='Private description', add_date="222",
add_date='4', private=False), tags="new-tag",
BookmarkHtmlTag(href='https://baz.com', add_date='444', tags='baz-tag') ),
BookmarkHtmlTag(
href="https://example.com/bar",
title="Updated Bar title",
description="Updated Bar description",
add_date="333",
tags="updated-bar-tag, updated-other-tag",
),
BookmarkHtmlTag(
href="https://example.com/unread",
title="Unread title",
description="Unread description",
add_date="3",
to_read=False,
),
BookmarkHtmlTag(
href="https://example.com/private",
title="Private title",
description="Private description",
add_date="4",
private=False,
),
BookmarkHtmlTag(href="https://baz.com", add_date="444", tags="baz-tag"),
] ]
# Import updated data # Import updated data
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
result = import_netscape_html(import_html, self.get_or_create_test_user(), ImportOptions(map_private_flag=True)) result = import_netscape_html(
import_html,
self.get_or_create_test_user(),
ImportOptions(map_private_flag=True),
)
# Check result # Check result
self.assertEqual(result.total, 6) self.assertEqual(result.total, 6)
@ -113,9 +189,9 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
def test_import_with_some_invalid_bookmarks(self): def test_import_with_some_invalid_bookmarks(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com'), BookmarkHtmlTag(href="https://example.com"),
# Invalid URL # Invalid URL
BookmarkHtmlTag(href='foo.com'), BookmarkHtmlTag(href="foo.com"),
# No URL # No URL
BookmarkHtmlTag(), BookmarkHtmlTag(),
] ]
@ -135,21 +211,23 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
def test_import_invalid_bookmark_does_not_associate_tags(self): def test_import_invalid_bookmark_does_not_associate_tags(self):
html_tags = [ html_tags = [
# No URL # No URL
BookmarkHtmlTag(tags='tag1, tag2, tag3'), BookmarkHtmlTag(tags="tag1, tag2, tag3"),
] ]
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
# Sqlite silently ignores relationships that have a non-persisted bookmark, # Sqlite silently ignores relationships that have a non-persisted bookmark,
# thus testing if the bulk create receives no relationships # thus testing if the bulk create receives no relationships
BookmarkToTagRelationShip = Bookmark.tags.through BookmarkToTagRelationShip = Bookmark.tags.through
with patch.object(BookmarkToTagRelationShip.objects, 'bulk_create') as mock_bulk_create: with patch.object(
BookmarkToTagRelationShip.objects, "bulk_create"
) as mock_bulk_create:
import_netscape_html(import_html, self.get_or_create_test_user()) import_netscape_html(import_html, self.get_or_create_test_user())
mock_bulk_create.assert_called_once_with([], ignore_conflicts=True) mock_bulk_create.assert_called_once_with([], ignore_conflicts=True)
def test_import_tags(self): def test_import_tags(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', tags='tag1'), BookmarkHtmlTag(href="https://example.com", tags="tag1"),
BookmarkHtmlTag(href='https://foo.com', tags='tag2'), BookmarkHtmlTag(href="https://foo.com", tags="tag2"),
BookmarkHtmlTag(href='https://bar.com', tags='tag3'), BookmarkHtmlTag(href="https://bar.com", tags="tag3"),
] ]
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
import_netscape_html(import_html, self.get_or_create_test_user()) import_netscape_html(import_html, self.get_or_create_test_user())
@ -158,16 +236,14 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
def test_create_missing_tags(self): def test_create_missing_tags(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', tags='tag1'), BookmarkHtmlTag(href="https://example.com", tags="tag1"),
BookmarkHtmlTag(href='https://foo.com', tags='tag2'), BookmarkHtmlTag(href="https://foo.com", tags="tag2"),
BookmarkHtmlTag(href='https://bar.com', tags='tag3'), BookmarkHtmlTag(href="https://bar.com", tags="tag3"),
] ]
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
import_netscape_html(import_html, self.get_or_create_test_user()) import_netscape_html(import_html, self.get_or_create_test_user())
html_tags.append( html_tags.append(BookmarkHtmlTag(href="https://baz.com", tags="tag4"))
BookmarkHtmlTag(href='https://baz.com', tags='tag4')
)
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
import_netscape_html(import_html, self.get_or_create_test_user()) import_netscape_html(import_html, self.get_or_create_test_user())
@ -175,9 +251,9 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
def test_create_missing_tags_does_not_duplicate_tags(self): def test_create_missing_tags_does_not_duplicate_tags(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', tags='tag1'), BookmarkHtmlTag(href="https://example.com", tags="tag1"),
BookmarkHtmlTag(href='https://foo.com', tags='tag1'), BookmarkHtmlTag(href="https://foo.com", tags="tag1"),
BookmarkHtmlTag(href='https://bar.com', tags='tag1'), BookmarkHtmlTag(href="https://bar.com", tags="tag1"),
] ]
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
import_netscape_html(import_html, self.get_or_create_test_user()) import_netscape_html(import_html, self.get_or_create_test_user())
@ -186,14 +262,12 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
def test_should_append_tags_to_bookmark_when_reimporting_with_different_tags(self): def test_should_append_tags_to_bookmark_when_reimporting_with_different_tags(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', tags='tag1'), BookmarkHtmlTag(href="https://example.com", tags="tag1"),
] ]
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
import_netscape_html(import_html, self.get_or_create_test_user()) import_netscape_html(import_html, self.get_or_create_test_user())
html_tags.append( html_tags.append(BookmarkHtmlTag(href="https://example.com", tags="tag2, tag3"))
BookmarkHtmlTag(href='https://example.com', tags='tag2, tag3')
)
import_html = self.render_html(tags=html_tags) import_html = self.render_html(tags=html_tags)
import_netscape_html(import_html, self.get_or_create_test_user()) import_netscape_html(import_html, self.get_or_create_test_user())
@ -202,65 +276,71 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
@override_settings(USE_TZ=False) @override_settings(USE_TZ=False)
def test_use_current_date_when_no_add_date(self): def test_use_current_date_when_no_add_date(self):
test_html = self.render_html(tags_html=f''' test_html = self.render_html(
tags_html=f"""
<DT><A HREF="https://example.com">Example.com</A> <DT><A HREF="https://example.com">Example.com</A>
<DD>Example.com <DD>Example.com
''') """
)
with patch.object(timezone, 'now', return_value=timezone.datetime(2021, 1, 1)): with patch.object(timezone, "now", return_value=timezone.datetime(2021, 1, 1)):
import_netscape_html(test_html, self.get_or_create_test_user()) import_netscape_html(test_html, self.get_or_create_test_user())
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].date_added, timezone.datetime(2021, 1, 1)) self.assertEqual(
Bookmark.objects.all()[0].date_added, timezone.datetime(2021, 1, 1)
)
def test_keep_title_if_imported_bookmark_has_empty_title(self): def test_keep_title_if_imported_bookmark_has_empty_title(self):
test_html = self.render_html(tags=[ test_html = self.render_html(
BookmarkHtmlTag(href='https://example.com', title='Example.com') tags=[BookmarkHtmlTag(href="https://example.com", title="Example.com")]
]) )
import_netscape_html(test_html, self.get_or_create_test_user()) import_netscape_html(test_html, self.get_or_create_test_user())
test_html = self.render_html(tags=[ test_html = self.render_html(tags=[BookmarkHtmlTag(href="https://example.com")])
BookmarkHtmlTag(href='https://example.com')
])
import_netscape_html(test_html, self.get_or_create_test_user()) import_netscape_html(test_html, self.get_or_create_test_user())
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].title, 'Example.com') self.assertEqual(Bookmark.objects.all()[0].title, "Example.com")
def test_keep_description_if_imported_bookmark_has_empty_description(self): def test_keep_description_if_imported_bookmark_has_empty_description(self):
test_html = self.render_html(tags=[ test_html = self.render_html(
BookmarkHtmlTag(href='https://example.com', description='Example.com') tags=[
]) BookmarkHtmlTag(href="https://example.com", description="Example.com")
]
)
import_netscape_html(test_html, self.get_or_create_test_user()) import_netscape_html(test_html, self.get_or_create_test_user())
test_html = self.render_html(tags=[ test_html = self.render_html(tags=[BookmarkHtmlTag(href="https://example.com")])
BookmarkHtmlTag(href='https://example.com')
])
import_netscape_html(test_html, self.get_or_create_test_user()) import_netscape_html(test_html, self.get_or_create_test_user())
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].description, 'Example.com') self.assertEqual(Bookmark.objects.all()[0].description, "Example.com")
def test_replace_whitespace_in_tag_names(self): def test_replace_whitespace_in_tag_names(self):
test_html = self.render_html(tags_html=f''' test_html = self.render_html(
tags_html=f"""
<DT><A HREF="https://example.com" TAGS="tag 1, tag 2, tag 3">Example.com</A> <DT><A HREF="https://example.com" TAGS="tag 1, tag 2, tag 3">Example.com</A>
<DD>Example.com <DD>Example.com
''') """
)
import_netscape_html(test_html, self.get_or_create_test_user()) import_netscape_html(test_html, self.get_or_create_test_user())
tags = Tag.objects.all() tags = Tag.objects.all()
tag_names = [tag.name for tag in tags] tag_names = [tag.name for tag in tags]
self.assertListEqual(tag_names, ['tag-1', 'tag-2', 'tag-3']) self.assertListEqual(tag_names, ["tag-1", "tag-2", "tag-3"])
@disable_logging @disable_logging
def test_validate_empty_or_missing_bookmark_url(self): def test_validate_empty_or_missing_bookmark_url(self):
test_html = self.render_html(tags_html=f''' test_html = self.render_html(
tags_html=f"""
<DT><A HREF="">Empty URL</A> <DT><A HREF="">Empty URL</A>
<DD>Empty URL <DD>Empty URL
<DT><A>Missing URL</A> <DT><A>Missing URL</A>
<DD>Missing URL <DD>Missing URL
''') """
)
import_result = import_netscape_html(test_html, self.get_or_create_test_user()) import_result = import_netscape_html(test_html, self.get_or_create_test_user())
@ -270,14 +350,16 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
def test_private_flag(self): def test_private_flag(self):
# does not map private flag if not enabled in options # does not map private flag if not enabled in options
test_html = self.render_html(tags_html=''' test_html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com/1" ADD_DATE="1">Example title 1</A> <DT><A HREF="https://example.com/1" ADD_DATE="1">Example title 1</A>
<DD>Example description 1</DD> <DD>Example description 1</DD>
<DT><A HREF="https://example.com/2" ADD_DATE="1" PRIVATE="1">Example title 2</A> <DT><A HREF="https://example.com/2" ADD_DATE="1" PRIVATE="1">Example title 2</A>
<DD>Example description 2</DD> <DD>Example description 2</DD>
<DT><A HREF="https://example.com/3" ADD_DATE="1" PRIVATE="0">Example title 3</A> <DT><A HREF="https://example.com/3" ADD_DATE="1" PRIVATE="0">Example title 3</A>
<DD>Example description 3</DD> <DD>Example description 3</DD>
''') """
)
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions()) import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 3) self.assertEqual(Bookmark.objects.count(), 3)
@ -287,23 +369,29 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
# does map private flag if enabled in options # does map private flag if enabled in options
Bookmark.objects.all().delete() Bookmark.objects.all().delete()
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions(map_private_flag=True)) import_netscape_html(
bookmark1 = Bookmark.objects.get(url='https://example.com/1') test_html,
bookmark2 = Bookmark.objects.get(url='https://example.com/2') self.get_or_create_test_user(),
bookmark3 = Bookmark.objects.get(url='https://example.com/3') ImportOptions(map_private_flag=True),
)
bookmark1 = Bookmark.objects.get(url="https://example.com/1")
bookmark2 = Bookmark.objects.get(url="https://example.com/2")
bookmark3 = Bookmark.objects.get(url="https://example.com/3")
self.assertEqual(bookmark1.shared, False) self.assertEqual(bookmark1.shared, False)
self.assertEqual(bookmark2.shared, False) self.assertEqual(bookmark2.shared, False)
self.assertEqual(bookmark3.shared, True) self.assertEqual(bookmark3.shared, True)
def test_archived_state(self): def test_archived_state(self):
test_html = self.render_html(tags_html=''' 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:archived">Example title 1</A>
<DD>Example description 1</DD> <DD>Example description 1</DD>
<DT><A HREF="https://example.com/2" ADD_DATE="1" PRIVATE="1" TAGS="tag1,tag2">Example title 2</A> <DT><A HREF="https://example.com/2" ADD_DATE="1" PRIVATE="1" TAGS="tag1,tag2">Example title 2</A>
<DD>Example description 2</DD> <DD>Example description 2</DD>
<DT><A HREF="https://example.com/3" ADD_DATE="1" PRIVATE="0">Example title 3</A> <DT><A HREF="https://example.com/3" ADD_DATE="1" PRIVATE="0">Example title 3</A>
<DD>Example description 3</DD> <DD>Example description 3</DD>
''') """
)
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions()) import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 3) self.assertEqual(Bookmark.objects.count(), 3)
@ -313,57 +401,67 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin, ImportTestMixin):
tags = Tag.objects.all() tags = Tag.objects.all()
self.assertEqual(len(tags), 2) self.assertEqual(len(tags), 2)
self.assertEqual(tags[0].name, 'tag1') self.assertEqual(tags[0].name, "tag1")
self.assertEqual(tags[1].name, 'tag2') self.assertEqual(tags[1].name, "tag2")
def test_notes(self): def test_notes(self):
# initial notes # initial notes
test_html = self.render_html(tags_html=''' test_html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description[linkding-notes]Example notes[/linkding-notes] <DD>Example description[linkding-notes]Example notes[/linkding-notes]
''') """
)
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions()) import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].description, 'Example description') self.assertEqual(Bookmark.objects.all()[0].description, "Example description")
self.assertEqual(Bookmark.objects.all()[0].notes, 'Example notes') self.assertEqual(Bookmark.objects.all()[0].notes, "Example notes")
# update notes # update notes
test_html = self.render_html(tags_html=''' test_html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description[linkding-notes]Updated notes[/linkding-notes] <DD>Example description[linkding-notes]Updated notes[/linkding-notes]
''') """
)
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions()) import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].description, 'Example description') self.assertEqual(Bookmark.objects.all()[0].description, "Example description")
self.assertEqual(Bookmark.objects.all()[0].notes, 'Updated notes') self.assertEqual(Bookmark.objects.all()[0].notes, "Updated notes")
# does not override existing notes if empty # does not override existing notes if empty
test_html = self.render_html(tags_html=''' test_html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description <DD>Example description
''') """
)
import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions()) import_netscape_html(test_html, self.get_or_create_test_user(), ImportOptions())
self.assertEqual(Bookmark.objects.count(), 1) self.assertEqual(Bookmark.objects.count(), 1)
self.assertEqual(Bookmark.objects.all()[0].description, 'Example description') self.assertEqual(Bookmark.objects.all()[0].description, "Example description")
self.assertEqual(Bookmark.objects.all()[0].notes, 'Updated notes') self.assertEqual(Bookmark.objects.all()[0].notes, "Updated notes")
def test_schedule_snapshot_creation(self): def test_schedule_snapshot_creation(self):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
test_html = self.render_html(tags_html='') test_html = self.render_html(tags_html="")
with patch.object(tasks, 'schedule_bookmarks_without_snapshots') as mock_schedule_bookmarks_without_snapshots: with patch.object(
tasks, "schedule_bookmarks_without_snapshots"
) as mock_schedule_bookmarks_without_snapshots:
import_netscape_html(test_html, user) import_netscape_html(test_html, user)
mock_schedule_bookmarks_without_snapshots.assert_called_once_with(user) mock_schedule_bookmarks_without_snapshots.assert_called_once_with(user)
def test_schedule_favicon_loading(self): def test_schedule_favicon_loading(self):
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
test_html = self.render_html(tags_html='') test_html = self.render_html(tags_html="")
with patch.object(tasks, 'schedule_bookmarks_without_favicons') as mock_schedule_bookmarks_without_favicons: with patch.object(
tasks, "schedule_bookmarks_without_favicons"
) as mock_schedule_bookmarks_without_favicons:
import_netscape_html(test_html, user) import_netscape_html(test_html, user)
mock_schedule_bookmarks_without_favicons.assert_called_once_with(user) mock_schedule_bookmarks_without_favicons.assert_called_once_with(user)

View file

@ -13,7 +13,7 @@ class MetadataViewTestCase(TestCase):
"short_name": "linkding", "short_name": "linkding",
"start_url": "bookmarks", "start_url": "bookmarks",
"display": "standalone", "display": "standalone",
"scope": "/" "scope": "/",
} }
self.assertDictEqual(response_body, expected_body) self.assertDictEqual(response_body, expected_body)
@ -28,6 +28,6 @@ class MetadataViewTestCase(TestCase):
"short_name": "linkding", "short_name": "linkding",
"start_url": "bookmarks", "start_url": "bookmarks",
"display": "standalone", "display": "standalone",
"scope": "/linkding/" "scope": "/linkding/",
} }
self.assertDictEqual(response_body, expected_body) self.assertDictEqual(response_body, expected_body)

View file

@ -13,18 +13,26 @@ class NavMenuTestCase(TestCase, BookmarkFactoryMixin):
def test_should_respect_share_profile_setting(self): def test_should_respect_share_profile_setting(self):
self.user.profile.enable_sharing = False self.user.profile.enable_sharing = False
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<a href="{reverse('bookmarks:shared')}" class="btn btn-link">Shared</a> <a href="{reverse('bookmarks:shared')}" class="btn btn-link">Shared</a>
''', html, count=0) """,
html,
count=0,
)
self.user.profile.enable_sharing = True self.user.profile.enable_sharing = True
self.user.profile.save() self.user.profile.save()
response = self.client.get(reverse('bookmarks:index')) response = self.client.get(reverse("bookmarks:index"))
html = response.content.decode() html = response.content.decode()
self.assertInHTML(f''' self.assertInHTML(
f"""
<a href="{reverse('bookmarks:shared')}" class="btn btn-link">Shared</a> <a href="{reverse('bookmarks:shared')}" class="btn btn-link">Shared</a>
''', html, count=2) """,
html,
count=2,
)

View file

@ -7,7 +7,9 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class PaginationTagTest(TestCase, BookmarkFactoryMixin): class PaginationTagTest(TestCase, BookmarkFactoryMixin):
def render_template(self, num_items: int, page_size: int, current_page: int, url: str = '/test') -> str: def render_template(
self, num_items: int, page_size: int, current_page: int, url: str = "/test"
) -> str:
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) request = rf.get(url)
request.user = self.get_or_create_test_user() request.user = self.get_or_create_test_user()
@ -15,58 +17,88 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
paginator = Paginator(range(0, num_items), page_size) paginator = Paginator(range(0, num_items), page_size)
page = paginator.page(current_page) page = paginator.page(current_page)
context = RequestContext(request, {'page': page}) context = RequestContext(request, {"page": page})
template_to_render = Template( template_to_render = Template("{% load pagination %}" "{% pagination page %}")
'{% load pagination %}'
'{% pagination page %}'
)
return template_to_render.render(context) return template_to_render.render(context)
def assertPrevLinkDisabled(self, html: str): def assertPrevLinkDisabled(self, html: str):
self.assertInHTML(''' self.assertInHTML(
"""
<li class="page-item disabled"> <li class="page-item disabled">
<a href="#" tabindex="-1">Previous</a> <a href="#" tabindex="-1">Previous</a>
</li> </li>
''', html) """,
html,
)
def assertPrevLink(self, html: str, page_number: int, href: str = None): def assertPrevLink(self, html: str, page_number: int, href: str = None):
href = href if href else '?page={0}'.format(page_number) href = href if href else "?page={0}".format(page_number)
self.assertInHTML(''' self.assertInHTML(
"""
<li class="page-item"> <li class="page-item">
<a href="{0}" tabindex="-1">Previous</a> <a href="{0}" tabindex="-1">Previous</a>
</li> </li>
'''.format(href), html) """.format(
href
),
html,
)
def assertNextLinkDisabled(self, html: str): def assertNextLinkDisabled(self, html: str):
self.assertInHTML(''' self.assertInHTML(
"""
<li class="page-item disabled"> <li class="page-item disabled">
<a href="#" tabindex="-1">Next</a> <a href="#" tabindex="-1">Next</a>
</li> </li>
''', html) """,
html,
)
def assertNextLink(self, html: str, page_number: int, href: str = None): def assertNextLink(self, html: str, page_number: int, href: str = None):
href = href if href else '?page={0}'.format(page_number) href = href if href else "?page={0}".format(page_number)
self.assertInHTML(''' self.assertInHTML(
"""
<li class="page-item"> <li class="page-item">
<a href="{0}" tabindex="-1">Next</a> <a href="{0}" tabindex="-1">Next</a>
</li> </li>
'''.format(href), html) """.format(
href
),
html,
)
def assertPageLink(self, html: str, page_number: int, active: bool, count: int = 1, href: str = None): def assertPageLink(
active_class = 'active' if active else '' self,
href = href if href else '?page={0}'.format(page_number) html: str,
self.assertInHTML(''' page_number: int,
active: bool,
count: int = 1,
href: str = None,
):
active_class = "active" if active else ""
href = href if href else "?page={0}".format(page_number)
self.assertInHTML(
"""
<li class="page-item {1}"> <li class="page-item {1}">
<a href="{2}">{0}</a> <a href="{2}">{0}</a>
</li> </li>
'''.format(page_number, active_class, href), html, count=count) """.format(
page_number, active_class, href
),
html,
count=count,
)
def assertTruncationIndicators(self, html: str, count: int): def assertTruncationIndicators(self, html: str, count: int):
self.assertInHTML(''' self.assertInHTML(
"""
<li class="page-item"> <li class="page-item">
<span>...</span> <span>...</span>
</li> </li>
''', html, count=count) """,
html,
count=count,
)
def test_previous_disabled_on_page_1(self): def test_previous_disabled_on_page_1(self):
rendered_template = self.render_template(100, 10, 1) rendered_template = self.render_template(100, 10, 1)
@ -92,7 +124,12 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
rendered_template = self.render_template(100, 10, current_page) rendered_template = self.render_template(100, 10, current_page)
for page_number in range(1, 10): for page_number in range(1, 10):
expected_occurrences = 1 if page_number in expected_visible_pages else 0 expected_occurrences = 1 if page_number in expected_visible_pages else 0
self.assertPageLink(rendered_template, page_number, page_number == current_page, expected_occurrences) self.assertPageLink(
rendered_template,
page_number,
page_number == current_page,
expected_occurrences,
)
self.assertTruncationIndicators(rendered_template, 1) self.assertTruncationIndicators(rendered_template, 1)
def test_truncate_pages_middle(self): def test_truncate_pages_middle(self):
@ -101,7 +138,12 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
rendered_template = self.render_template(100, 10, current_page) rendered_template = self.render_template(100, 10, current_page)
for page_number in range(1, 10): for page_number in range(1, 10):
expected_occurrences = 1 if page_number in expected_visible_pages else 0 expected_occurrences = 1 if page_number in expected_visible_pages else 0
self.assertPageLink(rendered_template, page_number, page_number == current_page, expected_occurrences) self.assertPageLink(
rendered_template,
page_number,
page_number == current_page,
expected_occurrences,
)
self.assertTruncationIndicators(rendered_template, 2) self.assertTruncationIndicators(rendered_template, 2)
def test_truncate_pages_near_end(self): def test_truncate_pages_near_end(self):
@ -110,12 +152,23 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
rendered_template = self.render_template(100, 10, current_page) rendered_template = self.render_template(100, 10, current_page)
for page_number in range(1, 10): for page_number in range(1, 10):
expected_occurrences = 1 if page_number in expected_visible_pages else 0 expected_occurrences = 1 if page_number in expected_visible_pages else 0
self.assertPageLink(rendered_template, page_number, page_number == current_page, expected_occurrences) self.assertPageLink(
rendered_template,
page_number,
page_number == current_page,
expected_occurrences,
)
self.assertTruncationIndicators(rendered_template, 1) self.assertTruncationIndicators(rendered_template, 1)
def test_respects_search_parameters(self): def test_respects_search_parameters(self):
rendered_template = self.render_template(100, 10, 2, url='/test?q=cake&sort=title_asc&page=2') rendered_template = self.render_template(
self.assertPrevLink(rendered_template, 1, href='?q=cake&sort=title_asc&page=1') 100, 10, 2, url="/test?q=cake&sort=title_asc&page=2"
self.assertPageLink(rendered_template, 1, False, href='?q=cake&sort=title_asc&page=1') )
self.assertPageLink(rendered_template, 2, True, href='?q=cake&sort=title_asc&page=2') self.assertPrevLink(rendered_template, 1, href="?q=cake&sort=title_asc&page=1")
self.assertNextLink(rendered_template, 3, href='?q=cake&sort=title_asc&page=3') self.assertPageLink(
rendered_template, 1, False, href="?q=cake&sort=title_asc&page=1"
)
self.assertPageLink(
rendered_template, 2, True, href="?q=cake&sort=title_asc&page=2"
)
self.assertNextLink(rendered_template, 3, href="?q=cake&sort=title_asc&page=3")

View file

@ -9,7 +9,9 @@ from bookmarks.tests.helpers import ImportTestMixin, BookmarkHtmlTag
class ParserTestCase(TestCase, ImportTestMixin): class ParserTestCase(TestCase, ImportTestMixin):
def assertTagsEqual(self, bookmarks: List[NetscapeBookmark], html_tags: List[BookmarkHtmlTag]): def assertTagsEqual(
self, bookmarks: List[NetscapeBookmark], html_tags: List[BookmarkHtmlTag]
):
self.assertEqual(len(bookmarks), len(html_tags)) self.assertEqual(len(bookmarks), len(html_tags))
for bookmark in bookmarks: for bookmark in bookmarks:
html_tag = html_tags[bookmarks.index(bookmark)] html_tag = html_tags[bookmarks.index(bookmark)]
@ -23,14 +25,34 @@ class ParserTestCase(TestCase, ImportTestMixin):
def test_parse_bookmarks(self): def test_parse_bookmarks(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='Example title', description='Example description', BookmarkHtmlTag(
add_date='1', tags='example-tag'), href="https://example.com",
BookmarkHtmlTag(href='https://example.com/foo', title='Foo title', description='', title="Example title",
add_date='2', tags=''), description="Example description",
BookmarkHtmlTag(href='https://example.com/bar', title='Bar title', description='Bar description', add_date="1",
add_date='3', tags='bar-tag, other-tag'), tags="example-tag",
BookmarkHtmlTag(href='https://example.com/baz', title='Baz title', description='Baz description', ),
add_date='3', to_read=True), BookmarkHtmlTag(
href="https://example.com/foo",
title="Foo title",
description="",
add_date="2",
tags="",
),
BookmarkHtmlTag(
href="https://example.com/bar",
title="Bar title",
description="Bar description",
add_date="3",
tags="bar-tag, other-tag",
),
BookmarkHtmlTag(
href="https://example.com/baz",
title="Baz title",
description="Baz description",
add_date="3",
to_read=True,
),
] ]
html = self.render_html(html_tags) html = self.render_html(html_tags)
bookmarks = parse(html) bookmarks = parse(html)
@ -45,10 +67,14 @@ class ParserTestCase(TestCase, ImportTestMixin):
def test_reset_properties_after_adding_bookmark(self): def test_reset_properties_after_adding_bookmark(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='Example title', description='Example description', BookmarkHtmlTag(
add_date='1', tags='example-tag'), href="https://example.com",
BookmarkHtmlTag(href='', title='', description='', title="Example title",
add_date='', tags='') description="Example description",
add_date="1",
tags="example-tag",
),
BookmarkHtmlTag(href="", title="", description="", add_date="", tags=""),
] ]
html = self.render_html(html_tags) html = self.render_html(html_tags)
bookmarks = parse(html) bookmarks = parse(html)
@ -57,59 +83,101 @@ class ParserTestCase(TestCase, ImportTestMixin):
def test_empty_title(self): def test_empty_title(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='', description='Example description', BookmarkHtmlTag(
add_date='1', tags='example-tag'), href="https://example.com",
title="",
description="Example description",
add_date="1",
tags="example-tag",
),
] ]
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1" TAGS="example-tag"></A> <DT><A HREF="https://example.com" ADD_DATE="1" TAGS="example-tag"></A>
<DD>Example description <DD>Example description
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertTagsEqual(bookmarks, html_tags) self.assertTagsEqual(bookmarks, html_tags)
def test_with_closing_description_tag(self): def test_with_closing_description_tag(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='Example title', description='Example description', BookmarkHtmlTag(
add_date='1', tags='example-tag'), href="https://example.com",
BookmarkHtmlTag(href='https://foo.com', title='Foo title', description='', title="Example title",
add_date='2', tags=''), description="Example description",
add_date="1",
tags="example-tag",
),
BookmarkHtmlTag(
href="https://foo.com",
title="Foo title",
description="",
add_date="2",
tags="",
),
] ]
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1" TAGS="example-tag">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1" TAGS="example-tag">Example title</A>
<DD>Example description</DD> <DD>Example description</DD>
<DT><A HREF="https://foo.com" ADD_DATE="2">Foo title</A> <DT><A HREF="https://foo.com" ADD_DATE="2">Foo title</A>
<DD></DD> <DD></DD>
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertTagsEqual(bookmarks, html_tags) self.assertTagsEqual(bookmarks, html_tags)
def test_description_tag_before_anchor_tag(self): def test_description_tag_before_anchor_tag(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='Example title', description='Example description', BookmarkHtmlTag(
add_date='1', tags='example-tag'), href="https://example.com",
BookmarkHtmlTag(href='https://foo.com', title='Foo title', description='', title="Example title",
add_date='2', tags=''), description="Example description",
add_date="1",
tags="example-tag",
),
BookmarkHtmlTag(
href="https://foo.com",
title="Foo title",
description="",
add_date="2",
tags="",
),
] ]
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><DD>Example description</DD> <DT><DD>Example description</DD>
<A HREF="https://example.com" ADD_DATE="1" TAGS="example-tag">Example title</A> <A HREF="https://example.com" ADD_DATE="1" TAGS="example-tag">Example title</A>
<DT><DD></DD> <DT><DD></DD>
<A HREF="https://foo.com" ADD_DATE="2">Foo title</A> <A HREF="https://foo.com" ADD_DATE="2">Foo title</A>
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertTagsEqual(bookmarks, html_tags) self.assertTagsEqual(bookmarks, html_tags)
def test_with_folders(self): def test_with_folders(self):
html_tags = [ html_tags = [
BookmarkHtmlTag(href='https://example.com', title='Example title', description='Example description', BookmarkHtmlTag(
add_date='1', tags='example-tag'), href="https://example.com",
BookmarkHtmlTag(href='https://foo.com', title='Foo title', description='', title="Example title",
add_date='2', tags=''), description="Example description",
add_date="1",
tags="example-tag",
),
BookmarkHtmlTag(
href="https://foo.com",
title="Foo title",
description="",
add_date="2",
tags="",
),
] ]
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DL><p> <DL><p>
<DT><H3>Folder 1</H3> <DT><H3>Folder 1</H3>
<DL><p> <DL><p>
@ -121,102 +189,126 @@ class ParserTestCase(TestCase, ImportTestMixin):
<DT><A HREF="https://foo.com" ADD_DATE="2">Foo title</A> <DT><A HREF="https://foo.com" ADD_DATE="2">Foo title</A>
</DL><p> </DL><p>
</DL><p> </DL><p>
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertTagsEqual(bookmarks, html_tags) self.assertTagsEqual(bookmarks, html_tags)
def test_private_flag(self): def test_private_flag(self):
# is private by default # is private by default
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description</DD> <DD>Example description</DD>
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].private, True) self.assertEqual(bookmarks[0].private, True)
# explicitly marked as private # explicitly marked as private
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1" PRIVATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1" PRIVATE="1">Example title</A>
<DD>Example description</DD> <DD>Example description</DD>
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].private, True) self.assertEqual(bookmarks[0].private, True)
# explicitly marked as public # explicitly marked as public
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1" PRIVATE="0">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1" PRIVATE="0">Example title</A>
<DD>Example description</DD> <DD>Example description</DD>
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].private, False) self.assertEqual(bookmarks[0].private, False)
def test_notes(self): def test_notes(self):
# no description, no notes # no description, no notes
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, '') self.assertEqual(bookmarks[0].description, "")
self.assertEqual(bookmarks[0].notes, '') self.assertEqual(bookmarks[0].notes, "")
# description, no notes # description, no notes
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description <DD>Example description
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, 'Example description') self.assertEqual(bookmarks[0].description, "Example description")
self.assertEqual(bookmarks[0].notes, '') self.assertEqual(bookmarks[0].notes, "")
# description, notes # description, notes
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description[linkding-notes]Example notes[/linkding-notes] <DD>Example description[linkding-notes]Example notes[/linkding-notes]
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, 'Example description') self.assertEqual(bookmarks[0].description, "Example description")
self.assertEqual(bookmarks[0].notes, 'Example notes') self.assertEqual(bookmarks[0].notes, "Example notes")
# description, notes without closing tag # description, notes without closing tag
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>Example description[linkding-notes]Example notes <DD>Example description[linkding-notes]Example notes
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, 'Example description') self.assertEqual(bookmarks[0].description, "Example description")
self.assertEqual(bookmarks[0].notes, 'Example notes') self.assertEqual(bookmarks[0].notes, "Example notes")
# no description, notes # no description, notes
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com" ADD_DATE="1">Example title</A>
<DD>[linkding-notes]Example notes[/linkding-notes] <DD>[linkding-notes]Example notes[/linkding-notes]
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, '') self.assertEqual(bookmarks[0].description, "")
self.assertEqual(bookmarks[0].notes, 'Example notes') self.assertEqual(bookmarks[0].notes, "Example notes")
# notes reset between bookmarks # notes reset between bookmarks
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com/1" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com/1" ADD_DATE="1">Example title</A>
<DD>[linkding-notes]Example notes[/linkding-notes] <DD>[linkding-notes]Example notes[/linkding-notes]
<DT><A HREF="https://example.com/2" ADD_DATE="1">Example title</A> <DT><A HREF="https://example.com/2" ADD_DATE="1">Example title</A>
<DD>Example description <DD>Example description
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].description, '') self.assertEqual(bookmarks[0].description, "")
self.assertEqual(bookmarks[0].notes, 'Example notes') self.assertEqual(bookmarks[0].notes, "Example notes")
self.assertEqual(bookmarks[1].description, 'Example description') self.assertEqual(bookmarks[1].description, "Example description")
self.assertEqual(bookmarks[1].notes, '') self.assertEqual(bookmarks[1].notes, "")
def test_unescape_content(self): def test_unescape_content(self):
html = self.render_html(tags_html=''' html = self.render_html(
tags_html="""
<DT><A HREF="https://example.com" ADD_DATE="1">&lt;style&gt;: The Style Information element</A> <DT><A HREF="https://example.com" ADD_DATE="1">&lt;style&gt;: The Style Information element</A>
<DD>The &lt;style&gt; HTML element contains style information for a document, or part of a document.[linkding-notes]Interesting notes about the &lt;style&gt; HTML element.[/linkding-notes] <DD>The &lt;style&gt; HTML element contains style information for a document, or part of a document.[linkding-notes]Interesting notes about the &lt;style&gt; HTML element.[/linkding-notes]
''') """
)
bookmarks = parse(html) bookmarks = parse(html)
self.assertEqual(bookmarks[0].title, self.assertEqual(bookmarks[0].title, "<style>: The Style Information element")
'<style>: The Style Information element') self.assertEqual(
self.assertEqual(bookmarks[0].description, bookmarks[0].description,
'The <style> HTML element contains style information for a document, or part of a document.') "The <style> HTML element contains style information for a document, or part of a document.",
self.assertEqual(bookmarks[0].notes, 'Interesting notes about the <style> HTML element.') )
self.assertEqual(
bookmarks[0].notes, "Interesting notes about the <style> HTML element."
)

View file

@ -7,49 +7,51 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class PasswordChangeViewTestCase(TestCase, BookmarkFactoryMixin): class PasswordChangeViewTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self) -> None: def setUp(self) -> None:
self.user = User.objects.create_user('testuser', 'test@example.com', 'initial_password') self.user = User.objects.create_user(
"testuser", "test@example.com", "initial_password"
)
self.client.force_login(self.user) self.client.force_login(self.user)
def test_change_password(self): def test_change_password(self):
form_data = { form_data = {
'old_password': 'initial_password', "old_password": "initial_password",
'new_password1': 'new_password', "new_password1": "new_password",
'new_password2': 'new_password', "new_password2": "new_password",
} }
response = self.client.post(reverse('change_password'), form_data) response = self.client.post(reverse("change_password"), form_data)
self.assertRedirects(response, reverse('password_change_done')) self.assertRedirects(response, reverse("password_change_done"))
def test_change_password_done(self): def test_change_password_done(self):
form_data = { form_data = {
'old_password': 'initial_password', "old_password": "initial_password",
'new_password1': 'new_password', "new_password1": "new_password",
'new_password2': 'new_password', "new_password2": "new_password",
} }
response = self.client.post(reverse('change_password'), form_data, follow=True) response = self.client.post(reverse("change_password"), form_data, follow=True)
self.assertContains(response, 'Your password was changed successfully') self.assertContains(response, "Your password was changed successfully")
def test_should_return_error_for_invalid_old_password(self): def test_should_return_error_for_invalid_old_password(self):
form_data = { form_data = {
'old_password': 'wrong_password', "old_password": "wrong_password",
'new_password1': 'new_password', "new_password1": "new_password",
'new_password2': 'new_password', "new_password2": "new_password",
} }
response = self.client.post(reverse('change_password'), form_data) response = self.client.post(reverse("change_password"), form_data)
self.assertIn('old_password', response.context_data['form'].errors) self.assertIn("old_password", response.context_data["form"].errors)
def test_should_return_error_for_mismatching_new_password(self): def test_should_return_error_for_mismatching_new_password(self):
form_data = { form_data = {
'old_password': 'initial_password', "old_password": "initial_password",
'new_password1': 'new_password', "new_password1": "new_password",
'new_password2': 'wrong_password', "new_password2": "wrong_password",
} }
response = self.client.post(reverse('change_password'), form_data) response = self.client.post(reverse("change_password"), form_data)
self.assertIn('new_password2', response.context_data['form'].errors) self.assertIn("new_password2", response.context_data["form"].errors)

File diff suppressed because it is too large Load diff

View file

@ -25,14 +25,13 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(tags=[self.setup_tag()], is_archived=True) self.setup_bookmark(tags=[self.setup_tag()], is_archived=True)
self.setup_bookmark(tags=[self.setup_tag()], is_archived=True) self.setup_bookmark(tags=[self.setup_tag()], is_archived=True)
response = self.client.get( response = self.client.get(reverse("bookmarks:settings.export"), follow=True)
reverse('bookmarks:settings.export'),
follow=True
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response['content-type'], 'text/plain; charset=UTF-8') self.assertEqual(response["content-type"], "text/plain; charset=UTF-8")
self.assertEqual(response['Content-Disposition'], 'attachment; filename="bookmarks.html"') self.assertEqual(
response["Content-Disposition"], 'attachment; filename="bookmarks.html"'
)
for bookmark in Bookmark.objects.all(): for bookmark in Bookmark.objects.all():
self.assertContains(response, bookmark.url) self.assertContains(response, bookmark.url)
@ -50,12 +49,9 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(tags=[self.setup_tag()], user=other_user), self.setup_bookmark(tags=[self.setup_tag()], user=other_user),
] ]
response = self.client.get( response = self.client.get(reverse("bookmarks:settings.export"), follow=True)
reverse('bookmarks:settings.export'),
follow=True
)
text = response.content.decode('utf-8') text = response.content.decode("utf-8")
for bookmark in owned_bookmarks: for bookmark in owned_bookmarks:
self.assertIn(bookmark.url, text) self.assertIn(bookmark.url, text)
@ -65,14 +61,22 @@ class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
def test_should_check_authentication(self): def test_should_check_authentication(self):
self.client.logout() self.client.logout()
response = self.client.get(reverse('bookmarks:settings.export'), follow=True) response = self.client.get(reverse("bookmarks:settings.export"), follow=True)
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.export')) self.assertRedirects(
response, reverse("login") + "?next=" + reverse("bookmarks:settings.export")
)
def test_should_show_hint_when_export_raises_error(self): def test_should_show_hint_when_export_raises_error(self):
with patch('bookmarks.services.exporter.export_netscape_html') as mock_export_netscape_html: with patch(
mock_export_netscape_html.side_effect = Exception('Nope') "bookmarks.services.exporter.export_netscape_html"
response = self.client.get(reverse('bookmarks:settings.export'), follow=True) ) as mock_export_netscape_html:
mock_export_netscape_html.side_effect = Exception("Nope")
response = self.client.get(
reverse("bookmarks:settings.export"), follow=True
)
self.assertTemplateUsed(response, 'settings/general.html') self.assertTemplateUsed(response, "settings/general.html")
self.assertFormErrorHint(response, 'An error occurred during bookmark export.') self.assertFormErrorHint(
response, "An error occurred during bookmark export."
)

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