diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..40c67a6 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 7ee249f..830d6e5 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,20 @@ python3 manage.py runserver ``` 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 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 ``` The frontend is now available under http://localhost:8000 - -Run all tests with pytest -``` -pytest -``` diff --git a/bookmarks/admin.py b/bookmarks/admin.py index 60efa93..b736b8b 100644 --- a/bookmarks/admin.py +++ b/bookmarks/admin.py @@ -14,80 +14,123 @@ from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark class LinkdingAdminSite(AdminSite): - site_header = 'linkding administration' - site_title = 'linkding Admin' + site_header = "linkding administration" + site_title = "linkding Admin" class AdminBookmark(admin.ModelAdmin): - list_display = ('resolved_title', 'url', 'is_archived', 'owner', 'date_added') - search_fields = ('title', 'description', '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'] + list_display = ("resolved_title", "url", "is_archived", "owner", "date_added") + search_fields = ( + "title", + "description", + "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): actions = super().get_actions(request) # 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 # 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 def delete_selected_bookmarks(self, request, queryset: QuerySet): bookmarks_count = queryset.count() for bookmark in queryset: bookmark.delete() - self.message_user(request, ngettext( - '%d bookmark was successfully deleted.', - '%d bookmarks were successfully deleted.', - bookmarks_count, - ) % bookmarks_count, messages.SUCCESS) + self.message_user( + request, + ngettext( + "%d bookmark was successfully deleted.", + "%d bookmarks were successfully deleted.", + bookmarks_count, + ) + % bookmarks_count, + messages.SUCCESS, + ) def archive_selected_bookmarks(self, request, queryset: QuerySet): for bookmark in queryset: archive_bookmark(bookmark) bookmarks_count = queryset.count() - self.message_user(request, ngettext( - '%d bookmark was successfully archived.', - '%d bookmarks were successfully archived.', - bookmarks_count, - ) % bookmarks_count, messages.SUCCESS) + self.message_user( + request, + ngettext( + "%d bookmark was successfully archived.", + "%d bookmarks were successfully archived.", + bookmarks_count, + ) + % bookmarks_count, + messages.SUCCESS, + ) def unarchive_selected_bookmarks(self, request, queryset: QuerySet): for bookmark in queryset: unarchive_bookmark(bookmark) bookmarks_count = queryset.count() - self.message_user(request, ngettext( - '%d bookmark was successfully unarchived.', - '%d bookmarks were successfully unarchived.', - bookmarks_count, - ) % bookmarks_count, messages.SUCCESS) + self.message_user( + request, + ngettext( + "%d bookmark was successfully unarchived.", + "%d bookmarks were successfully unarchived.", + bookmarks_count, + ) + % bookmarks_count, + messages.SUCCESS, + ) def mark_as_read(self, request, queryset: QuerySet): bookmarks_count = queryset.count() queryset.update(unread=False) - self.message_user(request, ngettext( - '%d bookmark marked as read.', - '%d bookmarks marked as read.', - bookmarks_count, - ) % bookmarks_count, messages.SUCCESS) + self.message_user( + request, + ngettext( + "%d bookmark marked as read.", + "%d bookmarks marked as read.", + bookmarks_count, + ) + % bookmarks_count, + messages.SUCCESS, + ) def mark_as_unread(self, request, queryset: QuerySet): bookmarks_count = queryset.count() queryset.update(unread=True) - self.message_user(request, ngettext( - '%d bookmark marked as unread.', - '%d bookmarks marked as unread.', - bookmarks_count, - ) % bookmarks_count, messages.SUCCESS) + self.message_user( + request, + ngettext( + "%d bookmark marked as unread.", + "%d bookmarks marked as unread.", + bookmarks_count, + ) + % bookmarks_count, + messages.SUCCESS, + ) class AdminTag(admin.ModelAdmin): - list_display = ('name', 'bookmarks_count', 'owner', 'date_added') - search_fields = ('name', 'owner__username') - list_filter = ('owner__username',) - ordering = ('-date_added',) - actions = ['delete_unused_tags'] + list_display = ("name", "bookmarks_count", "owner", "date_added") + search_fields = ("name", "owner__username") + list_filter = ("owner__username",) + ordering = ("-date_added",) + actions = ["delete_unused_tags"] def get_queryset(self, request): queryset = super().get_queryset(request) @@ -97,7 +140,7 @@ class AdminTag(admin.ModelAdmin): def bookmarks_count(self, obj): 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): unused_tags = queryset.filter(bookmark__isnull=True) @@ -106,23 +149,33 @@ class AdminTag(admin.ModelAdmin): tag.delete() if unused_tags_count > 0: - self.message_user(request, ngettext( - '%d unused tag was successfully deleted.', - '%d unused tags were successfully deleted.', - unused_tags_count, - ) % unused_tags_count, messages.SUCCESS) + self.message_user( + request, + ngettext( + "%d unused tag was successfully deleted.", + "%d unused tags were successfully deleted.", + unused_tags_count, + ) + % unused_tags_count, + messages.SUCCESS, + ) else: - self.message_user(request, gettext( - 'There were no unused tags in the selection', - ), messages.SUCCESS) + self.message_user( + request, + gettext( + "There were no unused tags in the selection", + ), + messages.SUCCESS, + ) class AdminUserProfileInline(admin.StackedInline): model = UserProfile can_delete = False - verbose_name_plural = 'Profile' - fk_name = 'user' - readonly_fields = ('search_preferences', ) + verbose_name_plural = "Profile" + fk_name = "user" + readonly_fields = ("search_preferences",) + class AdminCustomUser(UserAdmin): inlines = (AdminUserProfileInline,) @@ -134,15 +187,15 @@ class AdminCustomUser(UserAdmin): class AdminToast(admin.ModelAdmin): - list_display = ('key', 'message', 'owner', 'acknowledged') - search_fields = ('key', 'message') - list_filter = ('owner__username',) + list_display = ("key", "message", "owner", "acknowledged") + search_fields = ("key", "message") + list_filter = ("owner__username",) class AdminFeedToken(admin.ModelAdmin): - list_display = ('key', 'user') - search_fields = ['key'] - list_filter = ('user__username',) + list_display = ("key", "user") + search_fields = ["key"] + list_filter = ("user__username",) linkding_admin_site = LinkdingAdminSite() diff --git a/bookmarks/api/routes.py b/bookmarks/api/routes.py index 2cdd66b..bda6471 100644 --- a/bookmarks/api/routes.py +++ b/bookmarks/api/routes.py @@ -5,18 +5,28 @@ from rest_framework.response import Response from rest_framework.routers import DefaultRouter 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.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 -class BookmarkViewSet(viewsets.GenericViewSet, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.CreateModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin): +class BookmarkViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, +): serializer_class = BookmarkSerializer def get_permissions(self): @@ -24,7 +34,7 @@ class BookmarkViewSet(viewsets.GenericViewSet, # The shared action should still filter bookmarks so that # unauthenticated users only see bookmarks from users that have public # sharing explicitly enabled - if self.action == 'shared': + if self.action == "shared": return [AllowAny()] # Otherwise use default permissions which should require authentication @@ -33,7 +43,7 @@ class BookmarkViewSet(viewsets.GenericViewSet, def get_queryset(self): user = self.request.user # 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) return queries.query_bookmarks(user, user.profile, search) @@ -41,9 +51,9 @@ class BookmarkViewSet(viewsets.GenericViewSet, return Bookmark.objects.all().filter(owner=user) 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): user = request.user search = BookmarkSearch.from_request(request.GET) @@ -53,51 +63,59 @@ class BookmarkViewSet(viewsets.GenericViewSet, data = serializer(page, many=True).data return self.get_paginated_response(data) - @action(methods=['get'], detail=False) + @action(methods=["get"], detail=False) def shared(self, request): search = BookmarkSearch.from_request(request.GET) user = User.objects.filter(username=search.user).first() public_only = not request.user.is_authenticated - query_set = queries.query_shared_bookmarks(user, request.user_profile, search, public_only) + query_set = queries.query_shared_bookmarks( + user, request.user_profile, search, public_only + ) page = self.paginate_queryset(query_set) serializer = self.get_serializer_class() data = serializer(page, many=True).data return self.get_paginated_response(data) - @action(methods=['post'], detail=True) + @action(methods=["post"], detail=True) def archive(self, request, pk): bookmark = self.get_object() archive_bookmark(bookmark) return Response(status=status.HTTP_204_NO_CONTENT) - @action(methods=['post'], detail=True) + @action(methods=["post"], detail=True) def unarchive(self, request, pk): bookmark = self.get_object() unarchive_bookmark(bookmark) return Response(status=status.HTTP_204_NO_CONTENT) - @action(methods=['get'], detail=False) + @action(methods=["get"], detail=False) def check(self, request): - url = request.GET.get('url') + url = request.GET.get("url") 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 if bookmark: - metadata = WebsiteMetadata(url, bookmark.website_title, bookmark.website_description) + metadata = WebsiteMetadata( + url, bookmark.website_title, bookmark.website_description + ) else: metadata = website_loader.load_website_metadata(url) - return Response({ - 'bookmark': existing_bookmark_data, - 'metadata': metadata.to_dict() - }, status=status.HTTP_200_OK) + return Response( + {"bookmark": existing_bookmark_data, "metadata": metadata.to_dict()}, + status=status.HTTP_200_OK, + ) -class TagViewSet(viewsets.GenericViewSet, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.CreateModelMixin): +class TagViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, +): serializer_class = TagSerializer def get_queryset(self): @@ -105,16 +123,16 @@ class TagViewSet(viewsets.GenericViewSet, return Tag.objects.all().filter(owner=user) def get_serializer_context(self): - return {'user': self.request.user} + return {"user": self.request.user} class UserViewSet(viewsets.GenericViewSet): - @action(methods=['get'], detail=False) + @action(methods=["get"], detail=False) def profile(self, request): return Response(UserProfileSerializer(request.user.profile).data) router = DefaultRouter() -router.register(r'bookmarks', BookmarkViewSet, basename='bookmark') -router.register(r'tags', TagViewSet, basename='tag') -router.register(r'user', UserViewSet, basename='user') +router.register(r"bookmarks", BookmarkViewSet, basename="bookmark") +router.register(r"tags", TagViewSet, basename="tag") +router.register(r"user", UserViewSet, basename="user") diff --git a/bookmarks/api/serializers.py b/bookmarks/api/serializers.py index b1fcee8..6fd437a 100644 --- a/bookmarks/api/serializers.py +++ b/bookmarks/api/serializers.py @@ -14,7 +14,7 @@ class TagListField(serializers.ListField): class BookmarkListSerializer(ListSerializer): def to_representation(self, data): # Prefetch nested relations to avoid n+1 queries - prefetch_related_objects(data, 'tags') + prefetch_related_objects(data, "tags") return super().to_representation(data) @@ -23,32 +23,32 @@ class BookmarkSerializer(serializers.ModelSerializer): class Meta: model = Bookmark fields = [ - 'id', - 'url', - 'title', - 'description', - 'notes', - 'website_title', - 'website_description', - 'is_archived', - 'unread', - 'shared', - 'tag_names', - 'date_added', - 'date_modified' + "id", + "url", + "title", + "description", + "notes", + "website_title", + "website_description", + "is_archived", + "unread", + "shared", + "tag_names", + "date_added", + "date_modified", ] read_only_fields = [ - 'website_title', - 'website_description', - 'date_added', - 'date_modified' + "website_title", + "website_description", + "date_added", + "date_modified", ] list_serializer_class = BookmarkListSerializer # Override optional char fields to provide default value - title = 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='') + title = 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="") is_archived = serializers.BooleanField(required=False, default=False) unread = 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): bookmark = Bookmark() - bookmark.url = validated_data['url'] - bookmark.title = validated_data['title'] - bookmark.description = validated_data['description'] - bookmark.notes = validated_data['notes'] - bookmark.is_archived = validated_data['is_archived'] - bookmark.unread = validated_data['unread'] - bookmark.shared = validated_data['shared'] - tag_string = build_tag_string(validated_data['tag_names']) - return create_bookmark(bookmark, tag_string, self.context['user']) + bookmark.url = validated_data["url"] + bookmark.title = validated_data["title"] + bookmark.description = validated_data["description"] + bookmark.notes = validated_data["notes"] + bookmark.is_archived = validated_data["is_archived"] + bookmark.unread = validated_data["unread"] + bookmark.shared = validated_data["shared"] + tag_string = build_tag_string(validated_data["tag_names"]) + return create_bookmark(bookmark, tag_string, self.context["user"]) def update(self, instance: Bookmark, validated_data): # 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: setattr(instance, key, validated_data[key]) # Use tag string from payload, or use bookmark's current tags as fallback tag_string = build_tag_string(instance.tag_names) - if 'tag_names' in validated_data: - tag_string = build_tag_string(validated_data['tag_names']) + if "tag_names" in validated_data: + 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 Meta: model = Tag - fields = ['id', 'name', 'date_added'] - read_only_fields = ['date_added'] + fields = ["id", "name", "date_added"] + read_only_fields = ["date_added"] 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): diff --git a/bookmarks/apps.py b/bookmarks/apps.py index 89f1c7c..b58c2d9 100644 --- a/bookmarks/apps.py +++ b/bookmarks/apps.py @@ -2,7 +2,7 @@ from django.apps import AppConfig class BookmarksConfig(AppConfig): - name = 'bookmarks' + name = "bookmarks" def ready(self): # Register signal handlers diff --git a/bookmarks/context_processors.py b/bookmarks/context_processors.py index 71a33c7..8e79cf7 100644 --- a/bookmarks/context_processors.py +++ b/bookmarks/context_processors.py @@ -5,28 +5,32 @@ from bookmarks import utils def toasts(request): 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 return { - 'has_toasts': has_toasts, - 'toast_messages': toast_messages, + "has_toasts": has_toasts, + "toast_messages": toast_messages, } def public_shares(request): # Only check for public shares for anonymous users 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 return { - 'has_public_shares': has_public_shares, + "has_public_shares": has_public_shares, } return {} def app_version(request): - return { - 'app_version': utils.app_version - } + return {"app_version": utils.app_version} diff --git a/bookmarks/e2e/e2e_test_bookmark_form.py b/bookmarks/e2e/e2e_test_bookmark_form.py index a05639c..c4da421 100644 --- a/bookmarks/e2e/e2e_test_bookmark_form.py +++ b/bookmarks/e2e/e2e_test_bookmark_form.py @@ -6,38 +6,54 @@ from bookmarks.e2e.helpers import LinkdingE2ETestCase class BookmarkFormE2ETestCase(LinkdingE2ETestCase): def test_create_should_check_for_existing_bookmark(self): - existing_bookmark = self.setup_bookmark(title='Existing title', - description='Existing description', - notes='Existing notes', - tags=[self.setup_tag(name='tag1'), self.setup_tag(name='tag2')], - website_title='Existing website title', - website_description='Existing website description', - unread=True) - tag_names = ' '.join(existing_bookmark.tag_names) + existing_bookmark = self.setup_bookmark( + title="Existing title", + description="Existing description", + notes="Existing notes", + tags=[self.setup_tag(name="tag1"), self.setup_tag(name="tag2")], + website_title="Existing website title", + website_description="Existing website description", + unread=True, + ) + tag_names = " ".join(existing_bookmark.tag_names) with sync_playwright() as p: browser = self.setup_browser(p) 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 - page.get_by_label('URL').fill(existing_bookmark.url) + page.get_by_label("URL").fill(existing_bookmark.url) # Already bookmarked hint should be visible - page.get_by_text('This URL is already bookmarked.').wait_for(timeout=2000) + page.get_by_text("This URL is already bookmarked.").wait_for(timeout=2000) # Form should be pre-filled with data from existing bookmark - self.assertEqual(existing_bookmark.title, page.get_by_label('Title').input_value()) - self.assertEqual(existing_bookmark.description, page.get_by_label('Description').input_value()) - self.assertEqual(existing_bookmark.notes, page.get_by_label('Notes').input_value()) - self.assertEqual(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()) + self.assertEqual( + existing_bookmark.title, page.get_by_label("Title").input_value() + ) + self.assertEqual( + existing_bookmark.description, + page.get_by_label("Description").input_value(), + ) + self.assertEqual( + existing_bookmark.notes, page.get_by_label("Notes").input_value() + ) + self.assertEqual( + 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 - 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 - 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() @@ -47,21 +63,25 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase): with sync_playwright() as p: browser = self.setup_browser(p) 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.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): - 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: browser = self.setup_browser(p) 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') - expect(details).not_to_have_attribute('open', value='') + details = page.locator("details.notes") + expect(details).not_to_have_attribute("open", value="") - page.get_by_label('URL').fill(bookmark.url) - expect(details).to_have_attribute('open', value='') + page.get_by_label("URL").fill(bookmark.url) + expect(details).to_have_attribute("open", value="") diff --git a/bookmarks/e2e/e2e_test_bookmark_item.py b/bookmarks/e2e/e2e_test_bookmark_item.py index 796535f..6d7ddf9 100644 --- a/bookmarks/e2e/e2e_test_bookmark_item.py +++ b/bookmarks/e2e/e2e_test_bookmark_item.py @@ -9,15 +9,15 @@ from bookmarks.e2e.helpers import LinkdingE2ETestCase class BookmarkItemE2ETestCase(LinkdingE2ETestCase): @skip("Fails in CI, needs investigation") 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: - 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() - toggle_notes = page.locator('li button.toggle-notes') + toggle_notes = page.locator("li button.toggle-notes") toggle_notes.click() expect(notes).to_be_visible() diff --git a/bookmarks/e2e/e2e_test_bookmark_page_bulk_edit.py b/bookmarks/e2e/e2e_test_bookmark_page_bulk_edit.py index d9ca896..d58bf7d 100644 --- a/bookmarks/e2e/e2e_test_bookmark_page_bulk_edit.py +++ b/bookmarks/e2e/e2e_test_bookmark_page_bulk_edit.py @@ -9,100 +9,180 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): def setup_test_data(self): self.setup_numbered_bookmarks(50) self.setup_numbered_bookmarks(50, archived=True) - self.setup_numbered_bookmarks(50, prefix='foo') - self.setup_numbered_bookmarks(50, archived=True, prefix='foo') + self.setup_numbered_bookmarks(50, 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(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()) + self.assertEqual( + 50, + Bookmark.objects.filter( + 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): self.setup_test_data() 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_select_all().click() self.locate_bulk_edit_select_across().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.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.assertEqual(0, Bookmark.objects.filter(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()) + self.assertEqual( + 0, + Bookmark.objects.filter( + 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): self.setup_test_data() 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_select_all().click() self.locate_bulk_edit_select_across().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.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.assertEqual(50, Bookmark.objects.filter(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()) + self.assertEqual( + 50, + Bookmark.objects.filter( + 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): self.setup_test_data() 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_select_all().click() self.locate_bulk_edit_select_across().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.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.assertEqual(50, Bookmark.objects.filter(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()) + self.assertEqual( + 50, + Bookmark.objects.filter( + 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): self.setup_test_data() 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_select_all().click() self.locate_bulk_edit_select_across().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.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.assertEqual(50, Bookmark.objects.filter(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()) + self.assertEqual( + 50, + Bookmark.objects.filter( + 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): self.setup_numbered_bookmarks(5) with sync_playwright() as p: - url = reverse('bookmarks:index') + url = reverse("bookmarks:index") page = self.open(url, p) 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()) for i in range(checkboxes.count()): expect(checkboxes.nth(i)).not_to_be_checked() @@ -121,7 +201,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): self.setup_numbered_bookmarks(5) with sync_playwright() as p: - url = reverse('bookmarks:index') + url = reverse("bookmarks:index") self.open(url, p) self.locate_bulk_edit_toggle().click() @@ -138,7 +218,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): self.setup_numbered_bookmarks(5) with sync_playwright() as p: - url = reverse('bookmarks:index') + url = reverse("bookmarks:index") self.open(url, p) self.locate_bulk_edit_toggle().click() @@ -160,7 +240,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): self.setup_numbered_bookmarks(5) with sync_playwright() as p: - url = reverse('bookmarks:index') + url = reverse("bookmarks:index") self.open(url, p) self.locate_bulk_edit_toggle().click() @@ -171,18 +251,22 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): expect(self.locate_bulk_edit_select_across()).to_be_checked() # 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() # 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() def test_execute_resets_all_checkboxes(self): self.setup_numbered_bookmarks(100) with sync_playwright() as p: - url = reverse('bookmarks:index') + url = reverse("bookmarks:index") page = self.open(url, p) # Select all bookmarks, enable select across @@ -191,18 +275,18 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): self.locate_bulk_edit_select_across().click() # Get reference for bookmark list - bookmark_list = page.locator('ul[ld-bookmark-list]') + bookmark_list = page.locator("ul[ld-bookmark-list]") # Execute bulk action - 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('Confirm').click() + 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("Confirm").click() # Wait until bookmark list is updated (old reference becomes invisible) expect(bookmark_list).not_to_be_visible() # 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()) for i in range(checkboxes.count()): expect(checkboxes.nth(i)).not_to_be_checked() @@ -215,18 +299,22 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): self.setup_numbered_bookmarks(100) with sync_playwright() as p: - url = reverse('bookmarks:index') + url = reverse("bookmarks:index") self.open(url, p) self.locate_bulk_edit_toggle().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.locate_bulk_edit_bar().get_by_text('Execute').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.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() diff --git a/bookmarks/e2e/e2e_test_bookmark_page_partial_updates.py b/bookmarks/e2e/e2e_test_bookmark_page_partial_updates.py index f218bb2..2d77283 100644 --- a/bookmarks/e2e/e2e_test_bookmark_page_partial_updates.py +++ b/bookmarks/e2e/e2e_test_bookmark_page_partial_updates.py @@ -16,13 +16,15 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): # verify correct data is loaded on update self.setup_numbered_bookmarks(3, with_tags=True) self.setup_numbered_bookmarks(3, with_tags=True, archived=True) - self.setup_numbered_bookmarks(3, - shared=True, - prefix="Joe's Bookmark", - user=self.setup_user(enable_sharing=True)) + self.setup_numbered_bookmarks( + 3, + shared=True, + prefix="Joe's Bookmark", + user=self.setup_user(enable_sharing=True), + ) 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)) for title in titles: @@ -30,7 +32,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): expect(matching_tag).to_be_visible() 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)) for title in titles: @@ -38,65 +40,67 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): expect(matching_tag).to_be_visible() def test_partial_update_respects_query(self): - self.setup_numbered_bookmarks(5, prefix='foo') - self.setup_numbered_bookmarks(5, prefix='bar') + self.setup_numbered_bookmarks(5, prefix="foo") + self.setup_numbered_bookmarks(5, prefix="bar") with sync_playwright() as p: - url = reverse('bookmarks:index') + '?q=foo' + url = reverse("bookmarks:index") + "?q=foo" 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.assertVisibleBookmarks(['foo 1', 'foo 3', 'foo 4', 'foo 5']) + self.locate_bookmark("foo 2").get_by_text("Archive").click() + self.assertVisibleBookmarks(["foo 1", "foo 3", "foo 4", "foo 5"]) 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: - url = reverse('bookmarks:index') + '?sort=title_asc' + url = reverse("bookmarks:index") + "?sort=title_asc" page = self.open(url, p) - first_item = page.locator('li[ld-bookmark-item]').first - expect(first_item).to_contain_text('foo 1') + first_item = page.locator("li[ld-bookmark-item]").first + 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 - expect(first_item).to_contain_text('foo 2') + first_item = page.locator("li[ld-bookmark-item]").first + expect(first_item).to_contain_text("foo 2") def test_partial_update_respects_page(self): # 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: - url = reverse('bookmarks:index') + '?q=foo&page=2' + url = reverse("bookmarks:index") + "?q=foo&page=2" self.open(url, p) # 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.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) def test_multiple_partial_updates(self): self.setup_numbered_bookmarks(5) with sync_playwright() as p: - url = reverse('bookmarks:index') + url = reverse("bookmarks:index") self.open(url, p) - self.locate_bookmark('Bookmark 1').get_by_text('Archive').click() - self.assertVisibleBookmarks(['Bookmark 2', 'Bookmark 3', 'Bookmark 4', 'Bookmark 5']) + self.locate_bookmark("Bookmark 1").get_by_text("Archive").click() + self.assertVisibleBookmarks( + ["Bookmark 2", "Bookmark 3", "Bookmark 4", "Bookmark 5"] + ) - self.locate_bookmark('Bookmark 2').get_by_text('Archive').click() - self.assertVisibleBookmarks(['Bookmark 3', 'Bookmark 4', 'Bookmark 5']) + self.locate_bookmark("Bookmark 2").get_by_text("Archive").click() + self.assertVisibleBookmarks(["Bookmark 3", "Bookmark 4", "Bookmark 5"]) - self.locate_bookmark('Bookmark 3').get_by_text('Archive').click() - self.assertVisibleBookmarks(['Bookmark 4', 'Bookmark 5']) + self.locate_bookmark("Bookmark 3").get_by_text("Archive").click() + self.assertVisibleBookmarks(["Bookmark 4", "Bookmark 5"]) self.assertReloads(0) @@ -104,185 +108,201 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): self.setup_fixture() 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.assertVisibleTags(['Tag 1', 'Tag 3']) + self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"]) + self.assertVisibleTags(["Tag 1", "Tag 3"]) self.assertReloads(0) def test_active_bookmarks_partial_update_on_delete(self): self.setup_fixture() 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('Confirm').click() + self.locate_bookmark("Bookmark 2").get_by_text("Remove").click() + self.locate_bookmark("Bookmark 2").get_by_text("Confirm").click() - self.assertVisibleBookmarks(['Bookmark 1', 'Bookmark 3']) - self.assertVisibleTags(['Tag 1', 'Tag 3']) + self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"]) + self.assertVisibleTags(["Tag 1", "Tag 3"]) self.assertReloads(0) def test_active_bookmarks_partial_update_on_mark_as_read(self): self.setup_fixture() - bookmark2 = self.get_numbered_bookmark('Bookmark 2') + bookmark2 = self.get_numbered_bookmark("Bookmark 2") bookmark2.unread = True bookmark2.save() 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') - self.locate_bookmark('Bookmark 2').get_by_text('Unread').click() - self.locate_bookmark('Bookmark 2').get_by_text('Yes').click() + 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("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) def test_active_bookmarks_partial_update_on_unshare(self): self.setup_fixture() - bookmark2 = self.get_numbered_bookmark('Bookmark 2') + bookmark2 = self.get_numbered_bookmark("Bookmark 2") bookmark2.shared = True bookmark2.save() 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') - self.locate_bookmark('Bookmark 2').get_by_text('Shared').click() - self.locate_bookmark('Bookmark 2').get_by_text('Yes').click() + 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("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) def test_active_bookmarks_partial_update_on_bulk_archive(self): self.setup_fixture() 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_bookmark('Bookmark 2').locator('label[ld-bulk-edit-checkbox]').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.locate_bookmark("Bookmark 2").locator( + "label[ld-bulk-edit-checkbox]" + ).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.assertVisibleTags(['Tag 1', 'Tag 3']) + self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"]) + self.assertVisibleTags(["Tag 1", "Tag 3"]) self.assertReloads(0) def test_active_bookmarks_partial_update_on_bulk_delete(self): self.setup_fixture() 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_bookmark('Bookmark 2').locator('label[ld-bulk-edit-checkbox]').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.locate_bookmark("Bookmark 2").locator( + "label[ld-bulk-edit-checkbox]" + ).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.assertVisibleTags(['Tag 1', 'Tag 3']) + self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"]) + self.assertVisibleTags(["Tag 1", "Tag 3"]) self.assertReloads(0) def test_archived_bookmarks_partial_update_on_unarchive(self): self.setup_fixture() 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.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3']) + self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"]) + self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"]) self.assertReloads(0) def test_archived_bookmarks_partial_update_on_delete(self): self.setup_fixture() 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('Confirm').click() + self.locate_bookmark("Archived Bookmark 2").get_by_text("Remove").click() + self.locate_bookmark("Archived Bookmark 2").get_by_text("Confirm").click() - self.assertVisibleBookmarks(['Archived Bookmark 1', 'Archived Bookmark 3']) - self.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3']) + self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"]) + self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"]) self.assertReloads(0) def test_archived_bookmarks_partial_update_on_bulk_unarchive(self): self.setup_fixture() 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_bookmark('Archived Bookmark 2').locator('label[ld-bulk-edit-checkbox]').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.locate_bookmark("Archived Bookmark 2").locator( + "label[ld-bulk-edit-checkbox]" + ).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.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3']) + self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"]) + self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"]) self.assertReloads(0) def test_archived_bookmarks_partial_update_on_bulk_delete(self): self.setup_fixture() 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_bookmark('Archived Bookmark 2').locator('label[ld-bulk-edit-checkbox]').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.locate_bookmark("Archived Bookmark 2").locator( + "label[ld-bulk-edit-checkbox]" + ).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.assertVisibleTags(['Archived Tag 1', 'Archived Tag 3']) + self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"]) + self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"]) self.assertReloads(0) def test_shared_bookmarks_partial_update_on_unarchive(self): 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: - 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 - self.assertVisibleBookmarks([ - 'My Bookmark 1', - 'My Bookmark 2', - 'My Bookmark 3', - "Joe's Bookmark 1", - "Joe's Bookmark 2", - "Joe's Bookmark 3", - ]) - self.assertVisibleTags(['Shared Tag 1', 'Shared Tag 2', 'Shared Tag 3']) + self.assertVisibleBookmarks( + [ + "My Bookmark 1", + "My Bookmark 2", + "My Bookmark 3", + "Joe's Bookmark 1", + "Joe's Bookmark 2", + "Joe's Bookmark 3", + ] + ) + self.assertVisibleTags(["Shared Tag 1", "Shared Tag 2", "Shared Tag 3"]) self.assertReloads(0) def test_shared_bookmarks_partial_update_on_delete(self): 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: - 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('Confirm').click() + self.locate_bookmark("My Bookmark 2").get_by_text("Remove").click() + self.locate_bookmark("My Bookmark 2").get_by_text("Confirm").click() - self.assertVisibleBookmarks([ - 'My Bookmark 1', - 'My Bookmark 3', - "Joe's Bookmark 1", - "Joe's Bookmark 2", - "Joe's Bookmark 3", - ]) - self.assertVisibleTags(['Shared Tag 1', 'Shared Tag 3']) + self.assertVisibleBookmarks( + [ + "My Bookmark 1", + "My Bookmark 3", + "Joe's Bookmark 1", + "Joe's Bookmark 2", + "Joe's Bookmark 3", + ] + ) + self.assertVisibleTags(["Shared Tag 1", "Shared Tag 3"]) self.assertReloads(0) diff --git a/bookmarks/e2e/e2e_test_global_shortcuts.py b/bookmarks/e2e/e2e_test_global_shortcuts.py index 1d60b45..196d2aa 100644 --- a/bookmarks/e2e/e2e_test_global_shortcuts.py +++ b/bookmarks/e2e/e2e_test_global_shortcuts.py @@ -9,11 +9,11 @@ class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase): with sync_playwright() as p: browser = self.setup_browser(p) 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() @@ -21,10 +21,10 @@ class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase): with sync_playwright() as p: browser = self.setup_browser(p) 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() diff --git a/bookmarks/e2e/e2e_test_settings_general.py b/bookmarks/e2e/e2e_test_settings_general.py index 9e761f1..cdbf192 100644 --- a/bookmarks/e2e/e2e_test_settings_general.py +++ b/bookmarks/e2e/e2e_test_settings_general.py @@ -9,12 +9,14 @@ class SettingsGeneralE2ETestCase(LinkdingE2ETestCase): with sync_playwright() as p: browser = self.setup_browser(p) 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_label = page.get_by_text('Enable 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_sharing = page.get_by_label("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_label = page.get_by_text( + "Enable public bookmark sharing" + ) # Public sharing is disabled by default expect(enable_sharing).not_to_be_checked() diff --git a/bookmarks/e2e/helpers.py b/bookmarks/e2e/helpers.py index f3931a2..a6cbcd5 100644 --- a/bookmarks/e2e/helpers.py +++ b/bookmarks/e2e/helpers.py @@ -7,24 +7,28 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin): def setUp(self) -> None: 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: browser = playwright.chromium.launch(headless=True) context = browser.new_context() - context.add_cookies([{ - 'name': 'sessionid', - 'value': self.cookie.value, - 'domain': self.live_server_url.replace('http:', ''), - 'path': '/' - }]) + context.add_cookies( + [ + { + "name": "sessionid", + "value": self.cookie.value, + "domain": self.live_server_url.replace("http:", ""), + "path": "/", + } + ] + ) return context def open(self, url: str, playwright: Playwright) -> Page: browser = self.setup_browser(playwright) self.page = browser.new_page() 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 return self.page @@ -35,20 +39,24 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin): self.assertEqual(self.num_loads, count) 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) 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): - 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): - 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): - return self.page.get_by_title('Bulk edit') + return self.page.get_by_title("Bulk edit") 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) + ) diff --git a/bookmarks/feeds.py b/bookmarks/feeds.py index cec2129..7062818 100644 --- a/bookmarks/feeds.py +++ b/bookmarks/feeds.py @@ -17,17 +17,21 @@ class FeedContext: def sanitize(text: str): if not text: - return '' + return "" # remove control characters - valid_chars = ['\n', '\r', '\t'] - return ''.join(ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != 'C') + valid_chars = ["\n", "\r", "\t"] + return "".join( + ch for ch in text if ch in valid_chars or unicodedata.category(ch)[0] != "C" + ) class BaseBookmarksFeed(Feed): def get_object(self, request, feed_key: str): feed_token = FeedToken.objects.get(key__exact=feed_key) - search = BookmarkSearch(q=request.GET.get('q', '')) - query_set = queries.query_bookmarks(feed_token.user, feed_token.user.profile, search) + search = BookmarkSearch(q=request.GET.get("q", "")) + query_set = queries.query_bookmarks( + feed_token.user, feed_token.user.profile, search + ) return FeedContext(feed_token, query_set) def item_title(self, item: Bookmark): @@ -44,22 +48,22 @@ class BaseBookmarksFeed(Feed): class AllBookmarksFeed(BaseBookmarksFeed): - title = 'All bookmarks' - description = 'All bookmarks' + title = "All bookmarks" + description = "All bookmarks" 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): return context.query_set class UnreadBookmarksFeed(BaseBookmarksFeed): - title = 'Unread bookmarks' - description = 'All unread bookmarks' + title = "Unread bookmarks" + description = "All unread bookmarks" 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): return context.query_set.filter(unread=True) diff --git a/bookmarks/management/commands/backup.py b/bookmarks/management/commands/backup.py index f5dd806..94b2d3f 100644 --- a/bookmarks/management/commands/backup.py +++ b/bookmarks/management/commands/backup.py @@ -8,19 +8,19 @@ class Command(BaseCommand): help = "Creates a backup of the linkding database" 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): - destination = options['destination'] + destination = options["destination"] 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) with backup_db: source_db.backup(backup_db, pages=50, progress=progress) backup_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}")) diff --git a/bookmarks/management/commands/create_initial_superuser.py b/bookmarks/management/commands/create_initial_superuser.py index 9f44a0f..42b6440 100644 --- a/bookmarks/management/commands/create_initial_superuser.py +++ b/bookmarks/management/commands/create_initial_superuser.py @@ -12,18 +12,20 @@ class Command(BaseCommand): def handle(self, *args, **options): User = get_user_model() - superuser_name = os.getenv('LD_SUPERUSER_NAME', None) - superuser_password = os.getenv('LD_SUPERUSER_PASSWORD', None) + superuser_name = os.getenv("LD_SUPERUSER_NAME", None) + superuser_password = os.getenv("LD_SUPERUSER_PASSWORD", None) # Skip if option is undefined 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 # Skip if user already exists user_exists = User.objects.filter(username=superuser_name).exists() if user_exists: - logger.info('Skip creating initial superuser, user already exists') + logger.info("Skip creating initial superuser, user already exists") return user = User(username=superuser_name, is_superuser=True, is_staff=True) @@ -34,4 +36,4 @@ class Command(BaseCommand): user.set_unusable_password() user.save() - logger.info('Created initial superuser') + logger.info("Created initial superuser") diff --git a/bookmarks/management/commands/enable_wal.py b/bookmarks/management/commands/enable_wal.py index 6c66e3d..6e15c15 100644 --- a/bookmarks/management/commands/enable_wal.py +++ b/bookmarks/management/commands/enable_wal.py @@ -14,11 +14,11 @@ class Command(BaseCommand): if not settings.USE_SQLITE: return - connection = connections['default'] + connection = connections["default"] with connection.cursor() as cursor: cursor.execute("PRAGMA journal_mode") current_mode = cursor.fetchone()[0] - logger.info(f'Current journal mode: {current_mode}') - if current_mode != 'wal': + logger.info(f"Current journal mode: {current_mode}") + if current_mode != "wal": cursor.execute("PRAGMA journal_mode=wal;") - logger.info('Switched to WAL journal mode') + logger.info("Switched to WAL journal mode") diff --git a/bookmarks/management/commands/ensure_superuser.py b/bookmarks/management/commands/ensure_superuser.py index 06dbd2d..92656f8 100644 --- a/bookmarks/management/commands/ensure_superuser.py +++ b/bookmarks/management/commands/ensure_superuser.py @@ -6,13 +6,15 @@ class Command(BaseCommand): help = "Creates an admin user non-interactively if it doesn't exist" def add_arguments(self, parser): - parser.add_argument('--username', help="Admin's username") - parser.add_argument('--email', help="Admin's email") - parser.add_argument('--password', help="Admin's password") + parser.add_argument("--username", help="Admin's username") + parser.add_argument("--email", help="Admin's email") + parser.add_argument("--password", help="Admin's password") def handle(self, *args, **options): User = get_user_model() - if not User.objects.filter(username=options['username']).exists(): - User.objects.create_superuser(username=options['username'], - email=options['email'], - password=options['password']) + if not User.objects.filter(username=options["username"]).exists(): + User.objects.create_superuser( + username=options["username"], + email=options["email"], + password=options["password"], + ) diff --git a/bookmarks/management/commands/import_netscape.py b/bookmarks/management/commands/import_netscape.py index 5691584..e2fafdc 100644 --- a/bookmarks/management/commands/import_netscape.py +++ b/bookmarks/management/commands/import_netscape.py @@ -5,15 +5,17 @@ from bookmarks.services.importer import import_netscape_html class Command(BaseCommand): - help = 'Import Netscape HTML bookmark file' + help = "Import Netscape HTML bookmark file" def add_arguments(self, parser): - 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("file", type=str, help="Path to file") + parser.add_argument( + "user", type=str, help="Name of the user for which to import" + ) def handle(self, *args, **kwargs): - filepath = kwargs['file'] - username = kwargs['user'] + filepath = kwargs["file"] + username = kwargs["user"] with open(filepath) as html_file: html = html_file.read() user = User.objects.get(username=username) diff --git a/bookmarks/migrations/0001_initial.py b/bookmarks/migrations/0001_initial.py index a82f3f5..3c5dc85 100644 --- a/bookmarks/migrations/0001_initial.py +++ b/bookmarks/migrations/0001_initial.py @@ -15,19 +15,36 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Bookmark', + name="Bookmark", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.URLField()), - ('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)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.URLField()), + ("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, + ), + ), ], ), ] diff --git a/bookmarks/migrations/0002_auto_20190629_2303.py b/bookmarks/migrations/0002_auto_20190629_2303.py index 9d9a2e6..c63a2f3 100644 --- a/bookmarks/migrations/0002_auto_20190629_2303.py +++ b/bookmarks/migrations/0002_auto_20190629_2303.py @@ -9,22 +9,36 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('bookmarks', '0001_initial'), + ("bookmarks", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Tag', + name="Tag", fields=[ - ('id', models.AutoField(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)), + ( + "id", + models.AutoField( + 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( - model_name='bookmark', - name='tags', - field=models.ManyToManyField(to='bookmarks.Tag'), + model_name="bookmark", + name="tags", + field=models.ManyToManyField(to="bookmarks.Tag"), ), ] diff --git a/bookmarks/migrations/0003_auto_20200913_0656.py b/bookmarks/migrations/0003_auto_20200913_0656.py index 4c3758e..78ae8c3 100644 --- a/bookmarks/migrations/0003_auto_20200913_0656.py +++ b/bookmarks/migrations/0003_auto_20200913_0656.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0002_auto_20190629_2303'), + ("bookmarks", "0002_auto_20190629_2303"), ] operations = [ migrations.AlterField( - model_name='bookmark', - name='url', + model_name="bookmark", + name="url", field=models.URLField(max_length=2048), ), ] diff --git a/bookmarks/migrations/0004_auto_20200926_1028.py b/bookmarks/migrations/0004_auto_20200926_1028.py index 1636b0c..9b2f196 100644 --- a/bookmarks/migrations/0004_auto_20200926_1028.py +++ b/bookmarks/migrations/0004_auto_20200926_1028.py @@ -6,18 +6,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0003_auto_20200913_0656'), + ("bookmarks", "0003_auto_20200913_0656"), ] operations = [ migrations.AlterField( - model_name='bookmark', - name='description', + model_name="bookmark", + name="description", field=models.TextField(blank=True), ), migrations.AlterField( - model_name='bookmark', - name='title', + model_name="bookmark", + name="title", field=models.CharField(blank=True, max_length=512), ), ] diff --git a/bookmarks/migrations/0005_auto_20210103_1212.py b/bookmarks/migrations/0005_auto_20210103_1212.py index 11e5a63..d0d314a 100644 --- a/bookmarks/migrations/0005_auto_20210103_1212.py +++ b/bookmarks/migrations/0005_auto_20210103_1212.py @@ -7,13 +7,16 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0004_auto_20200926_1028'), + ("bookmarks", "0004_auto_20200926_1028"), ] operations = [ migrations.AlterField( - model_name='bookmark', - name='url', - field=models.CharField(max_length=2048, validators=[bookmarks.validators.BookmarkURLValidator()]), + model_name="bookmark", + name="url", + field=models.CharField( + max_length=2048, + validators=[bookmarks.validators.BookmarkURLValidator()], + ), ), ] diff --git a/bookmarks/migrations/0006_bookmark_is_archived.py b/bookmarks/migrations/0006_bookmark_is_archived.py index 21190e9..ab713f7 100644 --- a/bookmarks/migrations/0006_bookmark_is_archived.py +++ b/bookmarks/migrations/0006_bookmark_is_archived.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0005_auto_20210103_1212'), + ("bookmarks", "0005_auto_20210103_1212"), ] operations = [ migrations.AddField( - model_name='bookmark', - name='is_archived', + model_name="bookmark", + name="is_archived", field=models.BooleanField(default=False), ), ] diff --git a/bookmarks/migrations/0007_userprofile.py b/bookmarks/migrations/0007_userprofile.py index 20850b5..c9b229c 100644 --- a/bookmarks/migrations/0007_userprofile.py +++ b/bookmarks/migrations/0007_userprofile.py @@ -6,8 +6,8 @@ import django.db.models.deletion def forwards(apps, schema_editor): - User = apps.get_model('auth', 'User') - UserProfile = apps.get_model('bookmarks', 'UserProfile') + User = apps.get_model("auth", "User") + UserProfile = apps.get_model("bookmarks", "UserProfile") for user in User.objects.all(): try: if user.profile: @@ -24,19 +24,42 @@ def reverse(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('bookmarks', '0006_bookmark_is_archived'), + ("bookmarks", "0006_bookmark_is_archived"), ] operations = [ migrations.CreateModel( - name='UserProfile', + name="UserProfile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, 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)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + 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), diff --git a/bookmarks/migrations/0008_userprofile_bookmark_date_display.py b/bookmarks/migrations/0008_userprofile_bookmark_date_display.py index f27ce49..41dce4f 100644 --- a/bookmarks/migrations/0008_userprofile_bookmark_date_display.py +++ b/bookmarks/migrations/0008_userprofile_bookmark_date_display.py @@ -6,13 +6,21 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0007_userprofile'), + ("bookmarks", "0007_userprofile"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='bookmark_date_display', - field=models.CharField(choices=[('relative', 'Relative'), ('absolute', 'Absolute'), ('hidden', 'Hidden')], default='relative', max_length=10), + model_name="userprofile", + name="bookmark_date_display", + field=models.CharField( + choices=[ + ("relative", "Relative"), + ("absolute", "Absolute"), + ("hidden", "Hidden"), + ], + default="relative", + max_length=10, + ), ), ] diff --git a/bookmarks/migrations/0009_bookmark_web_archive_snapshot_url.py b/bookmarks/migrations/0009_bookmark_web_archive_snapshot_url.py index 89483d1..0f35c64 100644 --- a/bookmarks/migrations/0009_bookmark_web_archive_snapshot_url.py +++ b/bookmarks/migrations/0009_bookmark_web_archive_snapshot_url.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0008_userprofile_bookmark_date_display'), + ("bookmarks", "0008_userprofile_bookmark_date_display"), ] operations = [ migrations.AddField( - model_name='bookmark', - name='web_archive_snapshot_url', + model_name="bookmark", + name="web_archive_snapshot_url", field=models.CharField(blank=True, max_length=2048), ), ] diff --git a/bookmarks/migrations/0010_userprofile_bookmark_link_target.py b/bookmarks/migrations/0010_userprofile_bookmark_link_target.py index 90883d1..3c4e2e7 100644 --- a/bookmarks/migrations/0010_userprofile_bookmark_link_target.py +++ b/bookmarks/migrations/0010_userprofile_bookmark_link_target.py @@ -6,13 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0009_bookmark_web_archive_snapshot_url'), + ("bookmarks", "0009_bookmark_web_archive_snapshot_url"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='bookmark_link_target', - field=models.CharField(choices=[('_blank', 'New page'), ('_self', 'Same page')], default='_blank', max_length=10), + model_name="userprofile", + name="bookmark_link_target", + field=models.CharField( + choices=[("_blank", "New page"), ("_self", "Same page")], + default="_blank", + max_length=10, + ), ), ] diff --git a/bookmarks/migrations/0011_userprofile_web_archive_integration.py b/bookmarks/migrations/0011_userprofile_web_archive_integration.py index 309e4b0..dbc6c58 100644 --- a/bookmarks/migrations/0011_userprofile_web_archive_integration.py +++ b/bookmarks/migrations/0011_userprofile_web_archive_integration.py @@ -6,13 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0010_userprofile_bookmark_link_target'), + ("bookmarks", "0010_userprofile_bookmark_link_target"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='web_archive_integration', - field=models.CharField(choices=[('disabled', 'Disabled'), ('enabled', 'Enabled')], default='disabled', max_length=10), + model_name="userprofile", + name="web_archive_integration", + field=models.CharField( + choices=[("disabled", "Disabled"), ("enabled", "Enabled")], + default="disabled", + max_length=10, + ), ), ] diff --git a/bookmarks/migrations/0012_toast.py b/bookmarks/migrations/0012_toast.py index b4136c0..0fb0569 100644 --- a/bookmarks/migrations/0012_toast.py +++ b/bookmarks/migrations/0012_toast.py @@ -9,18 +9,32 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('bookmarks', '0011_userprofile_web_archive_integration'), + ("bookmarks", "0011_userprofile_web_archive_integration"), ] operations = [ migrations.CreateModel( - name='Toast', + name="Toast", fields=[ - ('id', models.AutoField(auto_created=True, 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)), + ( + "id", + models.AutoField( + auto_created=True, + 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, + ), + ), ], ), ] diff --git a/bookmarks/migrations/0013_web_archive_optin_toast.py b/bookmarks/migrations/0013_web_archive_optin_toast.py index c079a19..93627b9 100644 --- a/bookmarks/migrations/0013_web_archive_optin_toast.py +++ b/bookmarks/migrations/0013_web_archive_optin_toast.py @@ -10,19 +10,21 @@ User = get_user_model() def forwards(apps, schema_editor): for user in User.objects.all(): - toast = Toast(key='web_archive_opt_in_hint', - message='The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.', - owner=user) + toast = Toast( + key="web_archive_opt_in_hint", + message="The Internet Archive Wayback Machine integration has been disabled by default. Check the Settings to re-enable it.", + owner=user, + ) toast.save() 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): dependencies = [ - ('bookmarks', '0012_toast'), + ("bookmarks", "0012_toast"), ] operations = [ diff --git a/bookmarks/migrations/0014_alter_bookmark_unread.py b/bookmarks/migrations/0014_alter_bookmark_unread.py index 885b1b5..8e88021 100644 --- a/bookmarks/migrations/0014_alter_bookmark_unread.py +++ b/bookmarks/migrations/0014_alter_bookmark_unread.py @@ -4,7 +4,7 @@ from django.db import migrations, models def forwards(apps, schema_editor): - Bookmark = apps.get_model('bookmarks', 'Bookmark') + Bookmark = apps.get_model("bookmarks", "Bookmark") Bookmark.objects.update(unread=False) @@ -14,13 +14,13 @@ def reverse(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0013_web_archive_optin_toast'), + ("bookmarks", "0013_web_archive_optin_toast"), ] operations = [ migrations.AlterField( - model_name='bookmark', - name='unread', + model_name="bookmark", + name="unread", field=models.BooleanField(default=False), ), migrations.RunPython(forwards, reverse), diff --git a/bookmarks/migrations/0015_feedtoken.py b/bookmarks/migrations/0015_feedtoken.py index 15b4e0f..00d155d 100644 --- a/bookmarks/migrations/0015_feedtoken.py +++ b/bookmarks/migrations/0015_feedtoken.py @@ -9,16 +9,26 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('bookmarks', '0014_alter_bookmark_unread'), + ("bookmarks", "0014_alter_bookmark_unread"), ] operations = [ migrations.CreateModel( - name='FeedToken', + name="FeedToken", fields=[ - ('key', 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)), + ( + "key", + 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, + ), + ), ], ), ] diff --git a/bookmarks/migrations/0016_bookmark_shared.py b/bookmarks/migrations/0016_bookmark_shared.py index a780b44..c05ee32 100644 --- a/bookmarks/migrations/0016_bookmark_shared.py +++ b/bookmarks/migrations/0016_bookmark_shared.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0015_feedtoken'), + ("bookmarks", "0015_feedtoken"), ] operations = [ migrations.AddField( - model_name='bookmark', - name='shared', + model_name="bookmark", + name="shared", field=models.BooleanField(default=False), ), ] diff --git a/bookmarks/migrations/0017_userprofile_enable_sharing.py b/bookmarks/migrations/0017_userprofile_enable_sharing.py index bc676a2..7a094eb 100644 --- a/bookmarks/migrations/0017_userprofile_enable_sharing.py +++ b/bookmarks/migrations/0017_userprofile_enable_sharing.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0016_bookmark_shared'), + ("bookmarks", "0016_bookmark_shared"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='enable_sharing', + model_name="userprofile", + name="enable_sharing", field=models.BooleanField(default=False), ), ] diff --git a/bookmarks/migrations/0018_bookmark_favicon_file.py b/bookmarks/migrations/0018_bookmark_favicon_file.py index a61eb22..ce99fcb 100644 --- a/bookmarks/migrations/0018_bookmark_favicon_file.py +++ b/bookmarks/migrations/0018_bookmark_favicon_file.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0017_userprofile_enable_sharing'), + ("bookmarks", "0017_userprofile_enable_sharing"), ] operations = [ migrations.AddField( - model_name='bookmark', - name='favicon_file', + model_name="bookmark", + name="favicon_file", field=models.CharField(blank=True, max_length=512), ), ] diff --git a/bookmarks/migrations/0019_userprofile_enable_favicons.py b/bookmarks/migrations/0019_userprofile_enable_favicons.py index c64ff87..b0fc382 100644 --- a/bookmarks/migrations/0019_userprofile_enable_favicons.py +++ b/bookmarks/migrations/0019_userprofile_enable_favicons.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0018_bookmark_favicon_file'), + ("bookmarks", "0018_bookmark_favicon_file"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='enable_favicons', + model_name="userprofile", + name="enable_favicons", field=models.BooleanField(default=False), ), ] diff --git a/bookmarks/migrations/0020_userprofile_tag_search.py b/bookmarks/migrations/0020_userprofile_tag_search.py index 78711e7..13d9adf 100644 --- a/bookmarks/migrations/0020_userprofile_tag_search.py +++ b/bookmarks/migrations/0020_userprofile_tag_search.py @@ -6,13 +6,17 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0019_userprofile_enable_favicons'), + ("bookmarks", "0019_userprofile_enable_favicons"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='tag_search', - field=models.CharField(choices=[('strict', 'Strict'), ('lax', 'Lax')], default='strict', max_length=10), + model_name="userprofile", + name="tag_search", + field=models.CharField( + choices=[("strict", "Strict"), ("lax", "Lax")], + default="strict", + max_length=10, + ), ), ] diff --git a/bookmarks/migrations/0021_userprofile_display_url.py b/bookmarks/migrations/0021_userprofile_display_url.py index f44dce3..a4bcf7c 100644 --- a/bookmarks/migrations/0021_userprofile_display_url.py +++ b/bookmarks/migrations/0021_userprofile_display_url.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0020_userprofile_tag_search'), + ("bookmarks", "0020_userprofile_tag_search"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='display_url', + model_name="userprofile", + name="display_url", field=models.BooleanField(default=False), ), ] diff --git a/bookmarks/migrations/0022_bookmark_notes.py b/bookmarks/migrations/0022_bookmark_notes.py index 4670a61..b98df83 100644 --- a/bookmarks/migrations/0022_bookmark_notes.py +++ b/bookmarks/migrations/0022_bookmark_notes.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0021_userprofile_display_url'), + ("bookmarks", "0021_userprofile_display_url"), ] operations = [ migrations.AddField( - model_name='bookmark', - name='notes', + model_name="bookmark", + name="notes", field=models.TextField(blank=True), ), ] diff --git a/bookmarks/migrations/0023_userprofile_permanent_notes.py b/bookmarks/migrations/0023_userprofile_permanent_notes.py index 4bfc24e..6b7a1d5 100644 --- a/bookmarks/migrations/0023_userprofile_permanent_notes.py +++ b/bookmarks/migrations/0023_userprofile_permanent_notes.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0022_bookmark_notes'), + ("bookmarks", "0022_bookmark_notes"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='permanent_notes', + model_name="userprofile", + name="permanent_notes", field=models.BooleanField(default=False), ), ] diff --git a/bookmarks/migrations/0024_userprofile_enable_public_sharing.py b/bookmarks/migrations/0024_userprofile_enable_public_sharing.py index db5f40b..0a964d7 100644 --- a/bookmarks/migrations/0024_userprofile_enable_public_sharing.py +++ b/bookmarks/migrations/0024_userprofile_enable_public_sharing.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0023_userprofile_permanent_notes'), + ("bookmarks", "0023_userprofile_permanent_notes"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='enable_public_sharing', + model_name="userprofile", + name="enable_public_sharing", field=models.BooleanField(default=False), ), ] diff --git a/bookmarks/migrations/0025_userprofile_search_preferences.py b/bookmarks/migrations/0025_userprofile_search_preferences.py index 8886492..4cf1223 100644 --- a/bookmarks/migrations/0025_userprofile_search_preferences.py +++ b/bookmarks/migrations/0025_userprofile_search_preferences.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookmarks', '0024_userprofile_enable_public_sharing'), + ("bookmarks", "0024_userprofile_enable_public_sharing"), ] operations = [ migrations.AddField( - model_name='userprofile', - name='search_preferences', + model_name="userprofile", + name="search_preferences", field=models.JSONField(default=dict), ), ] diff --git a/bookmarks/models.py b/bookmarks/models.py index 2da99d7..87711ce 100644 --- a/bookmarks/models.py +++ b/bookmarks/models.py @@ -26,10 +26,10 @@ class Tag(models.Model): def sanitize_tag_name(tag_name: str): # strip leading/trailing spaces # 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: return [] names = tag_string.strip().split(delimiter) @@ -42,7 +42,7 @@ def parse_tag_string(tag_string: str, delimiter: str = ','): 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) @@ -82,7 +82,7 @@ class Bookmark(models.Model): return [tag.name for tag in self.tags.all()] def __str__(self): - return self.resolved_title + ' (' + self.url[:30] + '...)' + return self.resolved_title + " (" + self.url[:30] + "...)" class BookmarkForm(forms.ModelForm): @@ -90,15 +90,13 @@ class BookmarkForm(forms.ModelForm): url = forms.CharField(validators=[BookmarkURLValidator()]) tag_string = forms.CharField(required=False) # Do not require title and description in form as we fill these automatically if they are empty - title = forms.CharField(max_length=512, - required=False) - description = forms.CharField(required=False, - widget=forms.Textarea()) + title = forms.CharField(max_length=512, required=False) + description = forms.CharField(required=False, widget=forms.Textarea()) # Include website title and description as hidden field as they only provide info when editing bookmarks - website_title = forms.CharField(max_length=512, - required=False, widget=forms.HiddenInput()) - website_description = forms.CharField(required=False, - widget=forms.HiddenInput()) + website_title = forms.CharField( + max_length=512, required=False, widget=forms.HiddenInput() + ) + website_description = forms.CharField(required=False, widget=forms.HiddenInput()) unread = forms.BooleanField(required=False) shared = forms.BooleanField(required=False) # Hidden field that determines whether to close window/tab after saving the bookmark @@ -107,16 +105,16 @@ class BookmarkForm(forms.ModelForm): class Meta: model = Bookmark fields = [ - 'url', - 'tag_string', - 'title', - 'description', - 'notes', - 'website_title', - 'website_description', - 'unread', - 'shared', - 'auto_close', + "url", + "tag_string", + "title", + "description", + "notes", + "website_title", + "website_description", + "unread", + "shared", + "auto_close", ] @property @@ -125,45 +123,47 @@ class BookmarkForm(forms.ModelForm): class BookmarkSearch: - SORT_ADDED_ASC = 'added_asc' - SORT_ADDED_DESC = 'added_desc' - SORT_TITLE_ASC = 'title_asc' - SORT_TITLE_DESC = 'title_desc' + SORT_ADDED_ASC = "added_asc" + SORT_ADDED_DESC = "added_desc" + SORT_TITLE_ASC = "title_asc" + SORT_TITLE_DESC = "title_desc" - FILTER_SHARED_OFF = 'off' - FILTER_SHARED_SHARED = 'yes' - FILTER_SHARED_UNSHARED = 'no' + FILTER_SHARED_OFF = "off" + FILTER_SHARED_SHARED = "yes" + FILTER_SHARED_UNSHARED = "no" - FILTER_UNREAD_OFF = 'off' - FILTER_UNREAD_YES = 'yes' - FILTER_UNREAD_NO = 'no' + FILTER_UNREAD_OFF = "off" + FILTER_UNREAD_YES = "yes" + FILTER_UNREAD_NO = "no" - params = ['q', 'user', 'sort', 'shared', 'unread'] - preferences = ['sort', 'shared', 'unread'] + params = ["q", "user", "sort", "shared", "unread"] + preferences = ["sort", "shared", "unread"] defaults = { - 'q': '', - 'user': '', - 'sort': SORT_ADDED_DESC, - 'shared': FILTER_SHARED_OFF, - 'unread': FILTER_UNREAD_OFF, + "q": "", + "user": "", + "sort": SORT_ADDED_DESC, + "shared": FILTER_SHARED_OFF, + "unread": FILTER_UNREAD_OFF, } - def __init__(self, - q: str = None, - user: str = None, - sort: str = None, - shared: str = None, - unread: str = None, - preferences: dict = None): + def __init__( + self, + q: str = None, + user: str = None, + sort: str = None, + shared: str = None, + unread: str = None, + preferences: dict = None, + ): if not preferences: preferences = {} self.defaults = {**BookmarkSearch.defaults, **preferences} - self.q = q or self.defaults['q'] - self.user = user or self.defaults['user'] - self.sort = sort or self.defaults['sort'] - self.shared = shared or self.defaults['shared'] - self.unread = unread or self.defaults['unread'] + self.q = q or self.defaults["q"] + self.user = user or self.defaults["user"] + self.sort = sort or self.defaults["sort"] + self.shared = shared or self.defaults["shared"] + self.unread = unread or self.defaults["unread"] def is_modified(self, param): value = self.__dict__[param] @@ -175,7 +175,11 @@ class BookmarkSearch: @property 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 def has_modifications(self): @@ -191,7 +195,9 @@ class BookmarkSearch: @property 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 def from_request(query_dict: QueryDict, preferences: dict = None): @@ -206,20 +212,20 @@ class BookmarkSearch: class BookmarkSearchForm(forms.Form): SORT_CHOICES = [ - (BookmarkSearch.SORT_ADDED_ASC, 'Added ↑'), - (BookmarkSearch.SORT_ADDED_DESC, 'Added ↓'), - (BookmarkSearch.SORT_TITLE_ASC, 'Title ↑'), - (BookmarkSearch.SORT_TITLE_DESC, 'Title ↓'), + (BookmarkSearch.SORT_ADDED_ASC, "Added ↑"), + (BookmarkSearch.SORT_ADDED_DESC, "Added ↓"), + (BookmarkSearch.SORT_TITLE_ASC, "Title ↑"), + (BookmarkSearch.SORT_TITLE_DESC, "Title ↓"), ] FILTER_SHARED_CHOICES = [ - (BookmarkSearch.FILTER_SHARED_OFF, 'Off'), - (BookmarkSearch.FILTER_SHARED_SHARED, 'Shared'), - (BookmarkSearch.FILTER_SHARED_UNSHARED, 'Unshared'), + (BookmarkSearch.FILTER_SHARED_OFF, "Off"), + (BookmarkSearch.FILTER_SHARED_SHARED, "Shared"), + (BookmarkSearch.FILTER_SHARED_UNSHARED, "Unshared"), ] FILTER_UNREAD_CHOICES = [ - (BookmarkSearch.FILTER_UNREAD_OFF, 'Off'), - (BookmarkSearch.FILTER_UNREAD_YES, 'Unread'), - (BookmarkSearch.FILTER_UNREAD_NO, 'Read'), + (BookmarkSearch.FILTER_UNREAD_OFF, "Off"), + (BookmarkSearch.FILTER_UNREAD_YES, "Unread"), + (BookmarkSearch.FILTER_UNREAD_NO, "Read"), ] q = forms.CharField() @@ -228,7 +234,12 @@ class BookmarkSearchForm(forms.Form): shared = forms.ChoiceField(choices=FILTER_SHARED_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__() editable_fields = editable_fields or [] self.editable_fields = editable_fields @@ -236,8 +247,8 @@ class BookmarkSearchForm(forms.Form): # set choices for user field if users are provided if users: user_choices = [(user.username, user.username) for user in users] - user_choices.insert(0, ('', 'Everyone')) - self.fields['user'].choices = user_choices + user_choices.insert(0, ("", "Everyone")) + self.fields["user"].choices = user_choices for param in search.params: # set initial values for modified params @@ -251,50 +262,70 @@ class BookmarkSearchForm(forms.Form): class UserProfile(models.Model): - THEME_AUTO = 'auto' - THEME_LIGHT = 'light' - THEME_DARK = 'dark' + THEME_AUTO = "auto" + THEME_LIGHT = "light" + THEME_DARK = "dark" THEME_CHOICES = [ - (THEME_AUTO, 'Auto'), - (THEME_LIGHT, 'Light'), - (THEME_DARK, 'Dark'), + (THEME_AUTO, "Auto"), + (THEME_LIGHT, "Light"), + (THEME_DARK, "Dark"), ] - BOOKMARK_DATE_DISPLAY_RELATIVE = 'relative' - BOOKMARK_DATE_DISPLAY_ABSOLUTE = 'absolute' - BOOKMARK_DATE_DISPLAY_HIDDEN = 'hidden' + BOOKMARK_DATE_DISPLAY_RELATIVE = "relative" + BOOKMARK_DATE_DISPLAY_ABSOLUTE = "absolute" + BOOKMARK_DATE_DISPLAY_HIDDEN = "hidden" BOOKMARK_DATE_DISPLAY_CHOICES = [ - (BOOKMARK_DATE_DISPLAY_RELATIVE, 'Relative'), - (BOOKMARK_DATE_DISPLAY_ABSOLUTE, 'Absolute'), - (BOOKMARK_DATE_DISPLAY_HIDDEN, 'Hidden'), + (BOOKMARK_DATE_DISPLAY_RELATIVE, "Relative"), + (BOOKMARK_DATE_DISPLAY_ABSOLUTE, "Absolute"), + (BOOKMARK_DATE_DISPLAY_HIDDEN, "Hidden"), ] - BOOKMARK_LINK_TARGET_BLANK = '_blank' - BOOKMARK_LINK_TARGET_SELF = '_self' + BOOKMARK_LINK_TARGET_BLANK = "_blank" + BOOKMARK_LINK_TARGET_SELF = "_self" BOOKMARK_LINK_TARGET_CHOICES = [ - (BOOKMARK_LINK_TARGET_BLANK, 'New page'), - (BOOKMARK_LINK_TARGET_SELF, 'Same page'), + (BOOKMARK_LINK_TARGET_BLANK, "New page"), + (BOOKMARK_LINK_TARGET_SELF, "Same page"), ] - WEB_ARCHIVE_INTEGRATION_DISABLED = 'disabled' - WEB_ARCHIVE_INTEGRATION_ENABLED = 'enabled' + WEB_ARCHIVE_INTEGRATION_DISABLED = "disabled" + WEB_ARCHIVE_INTEGRATION_ENABLED = "enabled" WEB_ARCHIVE_INTEGRATION_CHOICES = [ - (WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'), - (WEB_ARCHIVE_INTEGRATION_ENABLED, 'Enabled'), + (WEB_ARCHIVE_INTEGRATION_DISABLED, "Disabled"), + (WEB_ARCHIVE_INTEGRATION_ENABLED, "Enabled"), ] - TAG_SEARCH_STRICT = 'strict' - TAG_SEARCH_LAX = 'lax' + TAG_SEARCH_STRICT = "strict" + TAG_SEARCH_LAX = "lax" TAG_SEARCH_CHOICES = [ - (TAG_SEARCH_STRICT, 'Strict'), - (TAG_SEARCH_LAX, 'Lax'), + (TAG_SEARCH_STRICT, "Strict"), + (TAG_SEARCH_LAX, "Lax"), ] - user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE) - theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO) - bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, 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) + user = models.OneToOneField( + get_user_model(), related_name="profile", on_delete=models.CASCADE + ) + theme = models.CharField( + max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO + ) + bookmark_date_display = models.CharField( + max_length=10, + choices=BOOKMARK_DATE_DISPLAY_CHOICES, + 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_public_sharing = 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 Meta: model = UserProfile - fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'tag_search', - 'enable_sharing', 'enable_public_sharing', 'enable_favicons', 'display_url', 'permanent_notes'] + fields = [ + "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()) @@ -332,11 +373,13 @@ class FeedToken(models.Model): """ Adapted from authtoken.models.Token """ + key = models.CharField(max_length=40, primary_key=True) - user = models.OneToOneField(get_user_model(), - related_name='feed_token', - on_delete=models.CASCADE, - ) + user = models.OneToOneField( + get_user_model(), + related_name="feed_token", + on_delete=models.CASCADE, + ) created = models.DateTimeField(auto_now_add=True) def save(self, *args, **kwargs): diff --git a/bookmarks/queries.py b/bookmarks/queries.py index dedeab6..6235e2f 100644 --- a/bookmarks/queries.py +++ b/bookmarks/queries.py @@ -10,18 +10,24 @@ from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile from bookmarks.utils import unique -def query_bookmarks(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet: - return _base_bookmarks_query(user, profile, search) \ - .filter(is_archived=False) +def query_bookmarks( + user: User, profile: UserProfile, search: BookmarkSearch +) -> QuerySet: + return _base_bookmarks_query(user, profile, search).filter(is_archived=False) -def query_archived_bookmarks(user: User, profile: UserProfile, search: BookmarkSearch) -> QuerySet: - return _base_bookmarks_query(user, profile, search) \ - .filter(is_archived=True) +def query_archived_bookmarks( + user: User, profile: UserProfile, search: BookmarkSearch +) -> QuerySet: + return _base_bookmarks_query(user, profile, search).filter(is_archived=True) -def query_shared_bookmarks(user: Optional[User], profile: UserProfile, search: BookmarkSearch, - public_only: bool) -> QuerySet: +def query_shared_bookmarks( + user: Optional[User], + profile: UserProfile, + search: BookmarkSearch, + public_only: bool, +) -> QuerySet: conditions = Q(shared=True) & Q(owner__profile__enable_sharing=True) if public_only: 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) -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 # Filter for user @@ -40,34 +48,32 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo query = parse_query_string(search.q) # Filter for search terms and tags - for term in query['search_terms']: - conditions = Q(title__icontains=term) \ - | Q(description__icontains=term) \ - | Q(notes__icontains=term) \ - | Q(website_title__icontains=term) \ - | Q(website_description__icontains=term) \ - | Q(url__icontains=term) + for term in query["search_terms"]: + conditions = ( + Q(title__icontains=term) + | Q(description__icontains=term) + | Q(notes__icontains=term) + | Q(website_title__icontains=term) + | Q(website_description__icontains=term) + | Q(url__icontains=term) + ) 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) - for tag_name in query['tag_names']: - query_set = query_set.filter( - tags__name__iexact=tag_name - ) + for tag_name in query["tag_names"]: + query_set = query_set.filter(tags__name__iexact=tag_name) # Untagged bookmarks - if query['untagged']: - query_set = query_set.filter( - tags=None - ) + if query["untagged"]: + query_set = query_set.filter(tags=None) # Legacy unread bookmarks filter from query - if query['unread']: - query_set = query_set.filter( - unread=True - ) + if query["unread"]: + query_set = query_set.filter(unread=True) # Unread filter from bookmark search 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 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: - query_set = query_set.order_by('-date_added') + query_set = query_set.order_by("-date_added") # 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 # to be replicated as there is no corresponding database field query_set = query_set.annotate( effective_title=Case( - 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')), - default=Lower('url'), - output_field=CharField() - )) + 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"), + ), + default=Lower("url"), + output_field=CharField(), + ) + ) # For SQLite, if the ICU extension is loaded, use the custom collation # loaded into the connection. This results in an improved sort order for # unicode characters (umlauts, etc.) 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: - order_field = 'effective_title' + order_field = "effective_title" if search.sort == BookmarkSearch.SORT_TITLE_ASC: 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 -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) 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() -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) 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() -def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, search: BookmarkSearch, - public_only: bool) -> QuerySet: +def query_shared_bookmark_tags( + user: Optional[User], + profile: UserProfile, + search: BookmarkSearch, + public_only: bool, +) -> QuerySet: bookmarks_query = query_shared_bookmarks(user, profile, search, public_only) 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() -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) 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): # Sanitize query params if not query_string: - query_string = '' + query_string = "" # 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] - search_terms = [word for word in keywords if word[0] != '#' and word[0] != '!'] - tag_names = [word[1:] for word in keywords if 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 = unique(tag_names, str.lower) # Special search commands - untagged = '!untagged' in keywords - unread = '!unread' in keywords + untagged = "!untagged" in keywords + unread = "!unread" in keywords return { - 'search_terms': search_terms, - 'tag_names': tag_names, - 'untagged': untagged, - 'unread': unread, + "search_terms": search_terms, + "tag_names": tag_names, + "untagged": untagged, + "unread": unread, } diff --git a/bookmarks/services/bookmarks.py b/bookmarks/services/bookmarks.py index 0ba5444..62db657 100644 --- a/bookmarks/services/bookmarks.py +++ b/bookmarks/services/bookmarks.py @@ -11,7 +11,9 @@ from bookmarks.services import tasks def create_bookmark(bookmark: Bookmark, tag_string: str, current_user: User): # 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: _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): sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) - Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(is_archived=True, - date_modified=timezone.now()) + Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( + is_archived=True, date_modified=timezone.now() + ) 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): sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) - Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(is_archived=False, - date_modified=timezone.now()) + Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( + is_archived=False, date_modified=timezone.now() + ) 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): 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', - flat=True) + owned_bookmark_ids = Bookmark.objects.filter( + owner=current_user, id__in=sanitized_bookmark_ids + ).values_list("id", flat=True) tag_names = parse_tag_string(tag_string) 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 = [] for tag in tags: 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 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) - owned_bookmark_ids = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).values_list('id', - flat=True) + owned_bookmark_ids = Bookmark.objects.filter( + owner=current_user, id__in=sanitized_bookmark_ids + ).values_list("id", flat=True) tag_names = parse_tag_string(tag_string) tags = get_or_create_tags(tag_names, current_user) BookmarkToTagRelationShip = Bookmark.tags.through for tag in tags: # 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): sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) - Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(unread=False, - date_modified=timezone.now()) + Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( + unread=False, date_modified=timezone.now() + ) def mark_bookmarks_as_unread(bookmark_ids: [Union[int, str]], current_user: User): sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) - Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(unread=True, - date_modified=timezone.now()) + Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( + unread=True, date_modified=timezone.now() + ) def share_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) - Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(shared=True, - date_modified=timezone.now()) + Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( + shared=True, date_modified=timezone.now() + ) def unshare_bookmarks(bookmark_ids: [Union[int, str]], current_user: User): sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids) - Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update(shared=False, - date_modified=timezone.now()) + Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids).update( + shared=False, date_modified=timezone.now() + ) def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark): diff --git a/bookmarks/services/exporter.py b/bookmarks/services/exporter.py index dc0717f..ebc3632 100644 --- a/bookmarks/services/exporter.py +++ b/bookmarks/services/exporter.py @@ -13,40 +13,41 @@ def export_netscape_html(bookmarks: List[Bookmark]): [append_bookmark(doc, bookmark) for bookmark in bookmarks] append_list_end(doc) - return '\n\r'.join(doc) + return "\n\r".join(doc) def append_header(doc: BookmarkDocument): - doc.append('') + doc.append("") doc.append('') - doc.append('
') + doc.append("
") def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark): url = bookmark.url - title = html.escape(bookmark.resolved_title or '') - desc = html.escape(bookmark.resolved_description or '') + title = html.escape(bookmark.resolved_title or "") + desc = html.escape(bookmark.resolved_description or "") 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 if bookmark.is_archived: - tag_names.append('linkding:archived') - tags = ','.join(tag_names) - toread = '1' if bookmark.unread else '0' - private = '0' if bookmark.shared else '1' + tag_names.append("linkding:archived") + tags = ",".join(tag_names) + toread = "1" if bookmark.unread else "0" + private = "0" if bookmark.shared else "1" added = int(bookmark.date_added.timestamp()) doc.append( - f'
') + doc.append("
") diff --git a/bookmarks/services/favicon_loader.py b/bookmarks/services/favicon_loader.py index 6534880..2798eb4 100644 --- a/bookmarks/services/favicon_loader.py +++ b/bookmarks/services/favicon_loader.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) # register mime type for .ico files, which is not included in the default # mimetypes of the Docker image -mimetypes.add_type('image/x-icon', '.ico') +mimetypes.add_type("image/x-icon", ".ico") def _ensure_favicon_folder(): @@ -23,16 +23,16 @@ def _ensure_favicon_folder(): 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: parsed_uri = urlparse(url) return { # 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 - 'domain': parsed_uri.hostname, + "domain": parsed_uri.hostname, } @@ -63,21 +63,21 @@ def load_favicon(url: str) -> str: # Create favicon folder if not exists _ensure_favicon_folder() # 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) if not favicon_file: # Load favicon from provider, save to file 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: - content_type = response.headers['Content-Type'] + content_type = response.headers["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) - with open(favicon_path, 'wb') as file: + with open(favicon_path, "wb") as file: for chunk in response.iter_content(chunk_size=8192): file.write(chunk) - logger.debug(f'Saved favicon as: {favicon_path}') + logger.debug(f"Saved favicon as: {favicon_path}") return favicon_file diff --git a/bookmarks/services/importer.py b/bookmarks/services/importer.py index 96f8fd0..cdef8f5 100644 --- a/bookmarks/services/importer.py +++ b/bookmarks/services/importer.py @@ -55,18 +55,20 @@ class TagCache: 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() import_start = timezone.now() try: netscape_bookmarks = parse(html) except: - logging.exception('Could not read bookmarks file.') + logging.exception("Could not read bookmarks file.") raise 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_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) end = timezone.now() - logger.debug(f'Import duration: {end - import_start}') + logger.debug(f"Import duration: {end - import_start}") return result @@ -110,7 +112,7 @@ def _get_batches(items: List, batch_size: int): num_items = len(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: batches.append(batch) offset = offset + batch_size @@ -118,11 +120,13 @@ def _get_batches(items: List, batch_size: int): return batches -def _import_batch(netscape_bookmarks: List[NetscapeBookmark], - user: User, - options: ImportOptions, - tag_cache: TagCache, - result: ImportResult): +def _import_batch( + netscape_bookmarks: List[NetscapeBookmark], + user: User, + options: ImportOptions, + tag_cache: TagCache, + result: ImportResult, +): # Query existing bookmarks batch_urls = [bookmark.href for bookmark in netscape_bookmarks] existing_bookmarks = Bookmark.objects.filter(owner=user, url__in=batch_urls) @@ -136,7 +140,13 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark], try: # Lookup existing bookmark by URL, or create new bookmark if there is no bookmark for that URL yet 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: bookmark = Bookmark(owner=user) is_update = False @@ -146,7 +156,7 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark], _copy_bookmark_data(netscape_bookmark, bookmark, options) # Validate bookmark fields, exclude owner to prevent n+1 database query, # also there is no specific validation on owner - bookmark.clean_fields(exclude=['owner']) + bookmark.clean_fields(exclude=["owner"]) # Schedule for update or insert if is_update: bookmarks_to_update.append(bookmark) @@ -155,20 +165,25 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark], result.success = result.success + 1 except: - shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + '...' - logging.exception('Error importing bookmark: ' + shortened_bookmark_tag_str) + shortened_bookmark_tag_str = str(netscape_bookmark)[:100] + "..." + logging.exception("Error importing bookmark: " + shortened_bookmark_tag_str) result.failed = result.failed + 1 # Bulk update bookmarks in DB - Bookmark.objects.bulk_update(bookmarks_to_update, ['url', - 'date_added', - 'date_modified', - 'unread', - 'shared', - 'title', - 'description', - 'notes', - 'owner']) + Bookmark.objects.bulk_update( + bookmarks_to_update, + [ + "url", + "date_added", + "date_modified", + "unread", + "shared", + "title", + "description", + "notes", + "owner", + ], + ) # Bulk insert new bookmarks into DB Bookmark.objects.bulk_create(bookmarks_to_create) @@ -183,13 +198,20 @@ def _import_batch(netscape_bookmarks: List[NetscapeBookmark], for netscape_bookmark in netscape_bookmarks: # Lookup bookmark by URL again 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: # 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( - 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 # 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) -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 if netscape_bookmark.date_added: bookmark.date_added = parse_timestamp(netscape_bookmark.date_added) diff --git a/bookmarks/services/parser.py b/bookmarks/services/parser.py index 0004dbc..81c4a41 100644 --- a/bookmarks/services/parser.py +++ b/bookmarks/services/parser.py @@ -25,29 +25,29 @@ class BookmarkParser(HTMLParser): self.current_tag = None self.bookmark = None - self.href = '' - self.add_date = '' - self.tags = '' - self.title = '' - self.description = '' - self.notes = '' - self.toread = '' - self.private = '' + self.href = "" + self.add_date = "" + self.tags = "" + self.title = "" + self.description = "" + self.notes = "" + self.toread = "" + self.private = "" def handle_starttag(self, tag: str, attrs: list): - name = 'handle_start_' + tag.lower() + name = "handle_start_" + tag.lower() if name in dir(self): getattr(self, name)({k.lower(): v for k, v in attrs}) self.current_tag = tag def handle_endtag(self, tag: str): - name = 'handle_end_' + tag.lower() + name = "handle_end_" + tag.lower() if name in dir(self): getattr(self, name)() self.current_tag = None def handle_data(self, data): - name = f'handle_{self.current_tag}_data' + name = f"handle_{self.current_tag}_data" if name in dir(self): getattr(self, name)(data) @@ -60,22 +60,22 @@ class BookmarkParser(HTMLParser): def handle_start_a(self, attrs: Dict[str, str]): vars(self).update(attrs) tag_names = parse_tag_string(self.tags) - archived = 'linkding:archived' in self.tags + archived = "linkding:archived" in self.tags try: - tag_names.remove('linkding:archived') + tag_names.remove("linkding:archived") except ValueError: pass self.bookmark = NetscapeBookmark( href=self.href, - title='', - description='', - notes='', + title="", + description="", + notes="", date_added=self.add_date, 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 - private=self.private != '0', + private=self.private != "0", archived=archived, ) @@ -84,9 +84,9 @@ class BookmarkParser(HTMLParser): def handle_dd_data(self, data): desc = data.strip() - if '[linkding-notes]' in desc: - self.notes = desc.split('[linkding-notes]')[1].split('[/linkding-notes]')[0] - self.description = desc.split('[linkding-notes]')[0] + if "[linkding-notes]" in desc: + self.notes = desc.split("[linkding-notes]")[1].split("[/linkding-notes]")[0] + self.description = desc.split("[linkding-notes]")[0] def add_bookmark(self): if self.bookmark: @@ -95,14 +95,14 @@ class BookmarkParser(HTMLParser): self.bookmark.notes = self.notes self.bookmarks.append(self.bookmark) self.bookmark = None - self.href = '' - self.add_date = '' - self.tags = '' - self.title = '' - self.description = '' - self.notes = '' - self.toread = '' - self.private = '' + self.href = "" + self.add_date = "" + self.tags = "" + self.title = "" + self.description = "" + self.notes = "" + self.toread = "" + self.private = "" def parse(html: str) -> List[NetscapeBookmark]: diff --git a/bookmarks/services/tags.py b/bookmarks/services/tags.py index d79a7fe..9d1cee1 100644 --- a/bookmarks/services/tags.py +++ b/bookmarks/services/tags.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) 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] - return unique(tags, operator.attrgetter('id')) + return unique(tags, operator.attrgetter("id")) def get_or_create_tag(name: str, user: User): diff --git a/bookmarks/services/tasks.py b/bookmarks/services/tasks.py index 3a57bf0..cb02e64 100644 --- a/bookmarks/services/tasks.py +++ b/bookmarks/services/tasks.py @@ -18,8 +18,10 @@ logger = logging.getLogger(__name__) def is_web_archive_integration_active(user: User) -> bool: background_tasks_enabled = not settings.LD_DISABLE_BACKGROUND_TASKS - web_archive_integration_enabled = \ - user.profile.web_archive_integration == UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED + web_archive_integration_enabled = ( + user.profile.web_archive_integration + == UserProfile.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): try: - logger.info(f'Load existing snapshot for bookmark. url={bookmark.url}') - cdx_api = bookmarks.services.wayback.CustomWaybackMachineCDXServerAPI(bookmark.url) + logger.info(f"Load existing snapshot for bookmark. url={bookmark.url}") + cdx_api = bookmarks.services.wayback.CustomWaybackMachineCDXServerAPI( + bookmark.url + ) existing_snapshot = cdx_api.newest() if existing_snapshot: bookmark.web_archive_snapshot_url = existing_snapshot.archive_url - bookmark.save(update_fields=['web_archive_snapshot_url']) - logger.info(f'Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}') + bookmark.save(update_fields=["web_archive_snapshot_url"]) + logger.info( + f"Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}" + ) 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: - 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): - logger.info(f'Create new snapshot for bookmark. url={bookmark.url}...') - archive = waybackpy.WaybackMachineSaveAPI(bookmark.url, DEFAULT_USER_AGENT, max_tries=1) + logger.info(f"Create new snapshot for bookmark. url={bookmark.url}...") + archive = waybackpy.WaybackMachineSaveAPI( + bookmark.url, DEFAULT_USER_AGENT, max_tries=1 + ) archive.save() bookmark.web_archive_snapshot_url = archive.archive_url - bookmark.save(update_fields=['web_archive_snapshot_url']) - logger.info(f'Successfully created new snapshot for bookmark:. url={bookmark.url}') + bookmark.save(update_fields=["web_archive_snapshot_url"]) + logger.info(f"Successfully created new snapshot for bookmark:. url={bookmark.url}") @background() @@ -72,10 +82,13 @@ def _create_web_archive_snapshot_task(bookmark_id: int, force_update: bool): return except TooManyRequestsError: 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: - logger.error(f'Failed to create snapshot, trying to load newest snapshot as fallback. url={bookmark.url}', - exc_info=error) + logger.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_newest_snapshot(bookmark) @@ -102,7 +115,9 @@ def schedule_bookmarks_without_snapshots(user: User): @background() def _schedule_bookmarks_without_snapshots_task(user_id: int): 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: # 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: 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) if new_favicon_file != bookmark.favicon_file: bookmark.favicon_file = new_favicon_file - bookmark.save(update_fields=['favicon_file']) - logger.info(f'Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon_file}') + bookmark.save(update_fields=["favicon_file"]) + logger.info( + f"Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon_file}" + ) def schedule_bookmarks_without_favicons(user: User): @@ -146,11 +163,13 @@ def schedule_bookmarks_without_favicons(user: User): @background() def _schedule_bookmarks_without_favicons_task(user_id: int): 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 = [] 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) Task.objects.bulk_create(tasks) @@ -168,7 +187,9 @@ def _schedule_refresh_favicons_task(user_id: int): tasks = [] 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) Task.objects.bulk_create(tasks) diff --git a/bookmarks/services/wayback.py b/bookmarks/services/wayback.py index d830403..b527b07 100644 --- a/bookmarks/services/wayback.py +++ b/bookmarks/services/wayback.py @@ -14,8 +14,10 @@ class CustomWaybackMachineCDXServerAPI(waybackpy.WaybackMachineCDXServerAPI): def newest(self): unix_timestamp = int(time.time()) - self.closest = waybackpy.utils.unix_timestamp_to_wayback_timestamp(unix_timestamp) - self.sort = 'closest' + self.closest = waybackpy.utils.unix_timestamp_to_wayback_timestamp( + unix_timestamp + ) + self.sort = "closest" self.limit = -5 newest_snapshot = None @@ -37,4 +39,4 @@ class CustomWaybackMachineCDXServerAPI(waybackpy.WaybackMachineCDXServerAPI): super().add_payload(payload) # 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 - payload['fastLatest'] = 'true' + payload["fastLatest"] = "true" diff --git a/bookmarks/services/website_loader.py b/bookmarks/services/website_loader.py index 7549c2c..644e2ea 100644 --- a/bookmarks/services/website_loader.py +++ b/bookmarks/services/website_loader.py @@ -18,9 +18,9 @@ class WebsiteMetadata: def to_dict(self): return { - 'url': self.url, - 'title': self.title, - 'description': self.description, + "url": self.url, + "title": self.title, + "description": self.description, } @@ -34,22 +34,29 @@ def load_website_metadata(url: str): start = timezone.now() page_text = load_page(url) end = timezone.now() - logger.debug(f'Load duration: {end - start}') + logger.debug(f"Load duration: {end - start}") 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 - description_tag = soup.find('meta', attrs={'name': 'description'}) - description = description_tag['content'].strip() if description_tag and description_tag[ - 'content'] else None + description_tag = soup.find("meta", attrs={"name": "description"}) + description = ( + description_tag["content"].strip() + if description_tag and description_tag["content"] + else None + ) if not 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_tag = soup.find("meta", attrs={"property": "og:description"}) + description = ( + description_tag["content"].strip() + if description_tag and description_tag["content"] + else None + ) end = timezone.now() - logger.debug(f'Parsing duration: {end - start}') + logger.debug(f"Parsing duration: {end - start}") finally: return WebsiteMetadata(url=url, title=title, description=description) @@ -73,30 +80,30 @@ def load_page(url: str): else: 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 - end_of_head = ''.encode('utf-8') + end_of_head = "".encode("utf-8") 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 break # Stop reading if we exceed 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 - if hasattr(r, '_content_consumed'): - logger.debug(f'Request consumed: {r._content_consumed}') + if hasattr(r, "_content_consumed"): + logger.debug(f"Request consumed: {r._content_consumed}") # 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 # This is different from Response.text which does respect the encoding specified in the response first, # before trying to determine one - results = from_bytes(content or '') + results = from_bytes(content or "") 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(): diff --git a/bookmarks/signals.py b/bookmarks/signals.py index 0fd3b15..8507c84 100644 --- a/bookmarks/signals.py +++ b/bookmarks/signals.py @@ -15,9 +15,11 @@ def user_logged_in(sender, request, user, **kwargs): def extend_sqlite(connection=None, **kwargs): # Load ICU extension into Sqlite connection to support case-insensitive # 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.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: # Load an ICU collation for case-insensitive ordering. diff --git a/bookmarks/templatetags/bookmarks.py b/bookmarks/templatetags/bookmarks.py index c8fcb07..af7f929 100644 --- a/bookmarks/templatetags/bookmarks.py +++ b/bookmarks/templatetags/bookmarks.py @@ -2,48 +2,67 @@ from typing import List 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.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): +@register.inclusion_tag("bookmarks/form.html", name="bookmark_form", takes_context=True) +def bookmark_form( + context, + form: BookmarkForm, + cancel_url: str, + bookmark_id: int = 0, + auto_close: bool = False, +): return { - 'request': context['request'], - 'form': form, - 'auto_close': auto_close, - 'bookmark_id': bookmark_id, - 'cancel_url': cancel_url + "request": context["request"], + "form": form, + "auto_close": auto_close, + "bookmark_id": bookmark_id, + "cancel_url": cancel_url, } -@register.inclusion_tag('bookmarks/search.html', name='bookmark_search', takes_context=True) -def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ''): +@register.inclusion_tag( + "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] - tags_string = build_tag_string(tag_names, ' ') - search_form = BookmarkSearchForm(search, editable_fields=['q']) + tags_string = build_tag_string(tag_names, " ") + search_form = BookmarkSearchForm(search, editable_fields=["q"]) - if mode == 'shared': - preferences_form = BookmarkSearchForm(search, editable_fields=['sort']) + if mode == "shared": + preferences_form = BookmarkSearchForm(search, editable_fields=["sort"]) else: - preferences_form = BookmarkSearchForm(search, editable_fields=['sort', 'shared', 'unread']) + preferences_form = BookmarkSearchForm( + search, editable_fields=["sort", "shared", "unread"] + ) return { - 'request': context['request'], - 'search': search, - 'search_form': search_form, - 'preferences_form': preferences_form, - 'tags_string': tags_string, - 'mode': mode, + "request": context["request"], + "search": search, + "search_form": search_form, + "preferences_form": preferences_form, + "tags_string": tags_string, + "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]): 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 { - 'search': search, - 'users': sorted_users, - 'form': form, + "search": search, + "users": sorted_users, + "form": form, } diff --git a/bookmarks/templatetags/pagination.py b/bookmarks/templatetags/pagination.py index 5d6fbd7..eff5900 100644 --- a/bookmarks/templatetags/pagination.py +++ b/bookmarks/templatetags/pagination.py @@ -8,14 +8,15 @@ NUM_ADJACENT_PAGES = 2 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): - 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 { - 'page': page, - 'visible_page_numbers': visible_page_numbers - } + return {"page": page, "visible_page_numbers": visible_page_numbers} 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() # Add adjacent pages around current page - visible_pages |= set(range( - max(1, current_page_number - NUM_ADJACENT_PAGES), - min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1 - )) + visible_pages |= set( + range( + max(1, current_page_number - NUM_ADJACENT_PAGES), + min(num_pages, current_page_number + NUM_ADJACENT_PAGES) + 1, + ) + ) # Add first page visible_pages.add(1) diff --git a/bookmarks/templatetags/shared.py b/bookmarks/templatetags/shared.py index c6acf22..6524c11 100644 --- a/bookmarks/templatetags/shared.py +++ b/bookmarks/templatetags/shared.py @@ -28,12 +28,12 @@ def add_tag_to_query(context, tag_name: str): params = context.request.GET.copy() # Append to or create query string - if params.__contains__('q'): - query_string = params.__getitem__('q') + ' ' + if params.__contains__("q"): + query_string = params.__getitem__("q") + " " else: - query_string = '' - query_string = query_string + '#' + tag_name - params.__setitem__('q', query_string) + query_string = "" + query_string = query_string + "#" + tag_name + params.__setitem__("q", query_string) return params.urlencode() @@ -41,20 +41,26 @@ def add_tag_to_query(context, tag_name: str): @register.simple_tag(takes_context=True) def remove_tag_from_query(context, tag_name: str): params = context.request.GET.copy() - if params.__contains__('q'): + if params.__contains__("q"): # Split query string into parts - query_string = params.__getitem__('q') + query_string = params.__getitem__("q") query_parts = query_string.split() # Remove tag with hash - tag_name_with_hash = '#' + tag_name - query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name_with_hash)] + tag_name_with_hash = "#" + tag_name + 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 profile = context.request.user_profile 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 - query_string = ' '.join(query_parts) - params.__setitem__('q', query_string) + query_string = " ".join(query_parts) + params.__setitem__("q", query_string) return params.urlencode() @@ -71,38 +77,38 @@ def replace_query_param(context, **kwargs): return query.urlencode() -@register.filter(name='hash_tag') +@register.filter(name="hash_tag") 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): return text[0] -@register.filter(name='remaining_chars') +@register.filter(name="remaining_chars") def remaining_chars(text, index): return text[index:] -@register.filter(name='humanize_absolute_date') +@register.filter(name="humanize_absolute_date") def humanize_absolute_date(value): - if value in (None, ''): - return '' + if value in (None, ""): + return "" return utils.humanize_absolute_date(value) -@register.filter(name='humanize_relative_date') +@register.filter(name="humanize_relative_date") def humanize_relative_date(value): - if value in (None, ''): - return '' + if value in (None, ""): + return "" return utils.humanize_relative_date(value) @register.tag def htmlmin(parser, token): - nodelist = parser.parse(('endhtmlmin',)) + nodelist = parser.parse(("endhtmlmin",)) parser.delete_first_token() return HtmlMinNode(nodelist) @@ -114,7 +120,7 @@ class HtmlMinNode(template.Node): def render(self, context): output = self.nodelist.render(context) - output = re.sub(r'\s+', ' ', output) + output = re.sub(r"\s+", " ", output) return output @@ -123,11 +129,11 @@ class HtmlMinNode(template.Node): def render_markdown(context, markdown_text): # naive approach to reusing the renderer for a single request # works for bookmark list for now - if not ('markdown_renderer' in context): - renderer = markdown.Markdown(extensions=['fenced_code', 'nl2br']) - context['markdown_renderer'] = renderer + if not ("markdown_renderer" in context): + renderer = markdown.Markdown(extensions=["fenced_code", "nl2br"]) + context["markdown_renderer"] = renderer else: - renderer = context['markdown_renderer'] + renderer = context["markdown_renderer"] as_html = renderer.convert(markdown_text) sanitized_html = bleach.clean(as_html, markdown_tags, markdown_attrs) diff --git a/bookmarks/tests/helpers.py b/bookmarks/tests/helpers.py index 2fdfa86..069be5f 100644 --- a/bookmarks/tests/helpers.py +++ b/bookmarks/tests/helpers.py @@ -18,26 +18,29 @@ class BookmarkFactoryMixin: def get_or_create_test_user(self): 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 - def setup_bookmark(self, - is_archived: bool = False, - unread: bool = False, - shared: bool = False, - tags=None, - user: User = None, - url: str = '', - title: str = None, - description: str = '', - notes: str = '', - website_title: str = '', - website_description: str = '', - web_archive_snapshot_url: str = '', - favicon_file: str = '', - added: datetime = None, - ): + def setup_bookmark( + self, + is_archived: bool = False, + unread: bool = False, + shared: bool = False, + tags=None, + user: User = None, + url: str = "", + title: str = None, + description: str = "", + notes: str = "", + website_title: str = "", + website_description: str = "", + web_archive_snapshot_url: str = "", + favicon_file: str = "", + added: datetime = None, + ): if title is None: title = get_random_string(length=32) if tags is None: @@ -46,7 +49,7 @@ class BookmarkFactoryMixin: user = self.get_or_create_test_user() if not url: unique_id = get_random_string(length=32) - url = 'https://example.com/' + unique_id + url = "https://example.com/" + unique_id if added is None: added = timezone.now() bookmark = Bookmark( @@ -71,49 +74,53 @@ class BookmarkFactoryMixin: bookmark.save() return bookmark - def setup_numbered_bookmarks(self, - count: int, - prefix: str = '', - suffix: str = '', - tag_prefix: str = '', - archived: bool = False, - unread: bool = False, - shared: bool = False, - with_tags: bool = False, - user: User = None): + def setup_numbered_bookmarks( + self, + count: int, + prefix: str = "", + suffix: str = "", + tag_prefix: str = "", + archived: bool = False, + unread: bool = False, + shared: bool = False, + with_tags: bool = False, + user: User = None, + ): user = user or self.get_or_create_test_user() bookmarks = [] if not prefix: if archived: - prefix = 'Archived Bookmark' + prefix = "Archived Bookmark" elif shared: - prefix = 'Shared Bookmark' + prefix = "Shared Bookmark" else: - prefix = 'Bookmark' + prefix = "Bookmark" if not tag_prefix: if archived: - tag_prefix = 'Archived Tag' + tag_prefix = "Archived Tag" elif shared: - tag_prefix = 'Shared Tag' + tag_prefix = "Shared Tag" else: - tag_prefix = 'Tag' + tag_prefix = "Tag" for i in range(1, count + 1): - title = f'{prefix} {i}{suffix}' - url = f'https://example.com/{prefix}/{i}' + title = f"{prefix} {i}{suffix}" + url = f"https://example.com/{prefix}/{i}" 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)] - bookmark = self.setup_bookmark(url=url, - title=title, - is_archived=archived, - unread=unread, - shared=shared, - tags=tags, - user=user) + bookmark = self.setup_bookmark( + url=url, + title=title, + is_archived=archived, + unread=unread, + shared=shared, + tags=tags, + user=user, + ) bookmarks.append(bookmark) return bookmarks @@ -121,7 +128,7 @@ class BookmarkFactoryMixin: def get_numbered_bookmark(self, title: str): 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: user = self.get_or_create_test_user() if not name: @@ -130,10 +137,15 @@ class BookmarkFactoryMixin: tag.save() 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: 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_public_sharing = enable_public_sharing user.profile.save() @@ -161,17 +173,17 @@ class LinkdingApiTestCase(APITestCase): return response 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) return response 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) return response 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) return response @@ -182,14 +194,16 @@ class LinkdingApiTestCase(APITestCase): class BookmarkHtmlTag: - def __init__(self, - href: str = '', - title: str = '', - description: str = '', - add_date: str = '', - tags: str = '', - to_read: bool = False, - private: bool = True): + def __init__( + self, + href: str = "", + title: str = "", + description: str = "", + add_date: str = "", + tags: str = "", + to_read: bool = False, + private: bool = True, + ): self.href = href self.title = title self.description = description @@ -201,7 +215,7 @@ class BookmarkHtmlTag: class ImportTestMixin: def render_tag(self, tag: BookmarkHtmlTag): - return f''' + return f"""
{tags_html}
- ''' + """ _words = [ - 'quasi', - 'consequatur', - 'necessitatibus', - 'debitis', - 'quod', - 'vero', - 'qui', - 'commodi', - 'quod', - 'odio', - 'aliquam', - 'veniam', - 'architecto', - 'consequatur', - 'autem', - 'qui', - 'iste', - 'asperiores', - 'soluta', - 'et', + "quasi", + "consequatur", + "necessitatibus", + "debitis", + "quod", + "vero", + "qui", + "commodi", + "quod", + "odio", + "aliquam", + "veniam", + "architecto", + "consequatur", + "autem", + "qui", + "iste", + "asperiores", + "soluta", + "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: num_words = random.randint(5, 10) 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) random.shuffle(selected_words) - return ' '.join(selected_words) + return " ".join(selected_words) def disable_logging(f): @@ -275,5 +289,5 @@ def disable_logging(f): def collapse_whitespace(text: str): - text = text.replace('\n', '').replace('\r', '') - return ' '.join(text.split()) + text = text.replace("\n", "").replace("\r", "") + return " ".join(text.split()) diff --git a/bookmarks/tests/test_anonymous_view.py b/bookmarks/tests/test_anonymous_view.py index 16ea3d4..8b8515f 100644 --- a/bookmarks/tests/test_anonymous_view.py +++ b/bookmarks/tests/test_anonymous_view.py @@ -6,21 +6,24 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin class AnonymousViewTestCase(TestCase, BookmarkFactoryMixin): def assertSharedBookmarksLinkCount(self, response, count): - url = reverse('bookmarks:shared') - self.assertContains(response, f'Shared bookmarks', - count=count) + url = reverse("bookmarks:shared") + self.assertContains( + response, + f'Shared bookmarks', + count=count, + ) def test_publicly_shared_bookmarks_link(self): # should not render link if no public shares exist user = self.setup_user(enable_sharing=True) self.setup_bookmark(user=user, shared=True) - response = self.client.get(reverse('login')) + response = self.client.get(reverse("login")) self.assertSharedBookmarksLinkCount(response, 0) # should render link if public shares exist user.profile.enable_public_sharing = True user.profile.save() - response = self.client.get(reverse('login')) + response = self.client.get(reverse("login")) self.assertSharedBookmarksLinkCount(response, 1) diff --git a/bookmarks/tests/test_app_options.py b/bookmarks/tests/test_app_options.py index f359184..391b023 100644 --- a/bookmarks/tests/test_app_options.py +++ b/bookmarks/tests/test_app_options.py @@ -7,23 +7,35 @@ from django.test import TestCase class AppOptionsTestCase(TestCase): def setUp(self) -> None: - self.settings_module = importlib.import_module('siteroot.settings.base') + self.settings_module = importlib.import_module("siteroot.settings.base") def test_empty_csrf_trusted_origins(self): 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): module = importlib.reload(self.settings_module) - self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS')) - self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com']) + self.assertTrue(hasattr(module, "CSRF_TRUSTED_ORIGINS")) + 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): module = importlib.reload(self.settings_module) - self.assertTrue(hasattr(module, 'CSRF_TRUSTED_ORIGINS')) - self.assertCountEqual(module.CSRF_TRUSTED_ORIGINS, ['https://linkding.example.com', 'http://linkding.example.com']) + self.assertTrue(hasattr(module, "CSRF_TRUSTED_ORIGINS")) + self.assertCountEqual( + module.CSRF_TRUSTED_ORIGINS, + ["https://linkding.example.com", "http://linkding.example.com"], + ) diff --git a/bookmarks/tests/test_auth_proxy_support.py b/bookmarks/tests/test_auth_proxy_support.py index 392119d..526c866 100644 --- a/bookmarks/tests/test_auth_proxy_support.py +++ b/bookmarks/tests/test_auth_proxy_support.py @@ -10,37 +10,49 @@ class AuthProxySupportTest(TestCase): # Reproducing configuration from the settings logic here # ideally this test would just override the respective options @modify_settings( - MIDDLEWARE={'append': 'bookmarks.middlewares.CustomRemoteUserMiddleware'}, - AUTHENTICATION_BACKENDS={'prepend': 'django.contrib.auth.backends.RemoteUserBackend'} + MIDDLEWARE={"append": "bookmarks.middlewares.CustomRemoteUserMiddleware"}, + AUTHENTICATION_BACKENDS={ + "prepend": "django.contrib.auth.backends.RemoteUserBackend" + }, ) 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} - response = self.client.get(reverse('bookmarks:index'), **headers) + headers = {"REMOTE_USER": user.username} + response = self.client.get(reverse("bookmarks:index"), **headers) self.assertEqual(response.status_code, 200) # Reproducing configuration from the settings logic here # ideally this test would just override the respective options @modify_settings( - MIDDLEWARE={'append': 'bookmarks.middlewares.CustomRemoteUserMiddleware'}, - AUTHENTICATION_BACKENDS={'prepend': 'django.contrib.auth.backends.RemoteUserBackend'} + MIDDLEWARE={"append": "bookmarks.middlewares.CustomRemoteUserMiddleware"}, + AUTHENTICATION_BACKENDS={ + "prepend": "django.contrib.auth.backends.RemoteUserBackend" + }, ) def test_auth_proxy_with_custom_header(self): - with patch.object(CustomRemoteUserMiddleware, 'header', new_callable=PropertyMock) as mock: - mock.return_value = 'Custom-User' - user = User.objects.create_user('auth_proxy_user', 'user@example.com', 'password123') + with patch.object( + CustomRemoteUserMiddleware, "header", new_callable=PropertyMock + ) as mock: + mock.return_value = "Custom-User" + user = User.objects.create_user( + "auth_proxy_user", "user@example.com", "password123" + ) - headers = {'Custom-User': user.username} - response = self.client.get(reverse('bookmarks:index'), **headers) + headers = {"Custom-User": user.username} + response = self.client.get(reverse("bookmarks:index"), **headers) self.assertEqual(response.status_code, 200) 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} - response = self.client.get(reverse('bookmarks:index'), **headers, follow=True) + headers = {"REMOTE_USER": user.username} + response = self.client.get(reverse("bookmarks:index"), **headers, follow=True) - self.assertRedirects(response, '/login/?next=%2Fbookmarks') + self.assertRedirects(response, "/login/?next=%2Fbookmarks") diff --git a/bookmarks/tests/test_bookmark_action_view.py b/bookmarks/tests/test_bookmark_action_view.py index ca5fe69..658cb5a 100644 --- a/bookmarks/tests/test_bookmark_action_view.py +++ b/bookmarks/tests/test_bookmark_action_view.py @@ -17,26 +17,37 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): self.assertEqual(len(bookmarks), Bookmark.objects.count()) 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): bookmark = self.setup_bookmark() - self.client.post(reverse('bookmarks:index.action'), { - 'archive': [bookmark.id], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "archive": [bookmark.id], + }, + ) bookmark.refresh_from_db() self.assertTrue(bookmark.is_archived) 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) - response = self.client.post(reverse('bookmarks:index.action'), { - 'archive': [bookmark.id], - }) + response = self.client.post( + reverse("bookmarks:index.action"), + { + "archive": [bookmark.id], + }, + ) bookmark.refresh_from_db() @@ -46,20 +57,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): def test_unarchive_should_unarchive_bookmark(self): bookmark = self.setup_bookmark(is_archived=True) - self.client.post(reverse('bookmarks:index.action'), { - 'unarchive': [bookmark.id], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "unarchive": [bookmark.id], + }, + ) bookmark.refresh_from_db() self.assertFalse(bookmark.is_archived) 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) - response = self.client.post(reverse('bookmarks:index.action'), { - 'unarchive': [bookmark.id], - }) + response = self.client.post( + reverse("bookmarks:index.action"), + { + "unarchive": [bookmark.id], + }, + ) bookmark.refresh_from_db() self.assertEqual(response.status_code, 404) @@ -68,28 +87,39 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): def test_delete_should_delete_bookmark(self): bookmark = self.setup_bookmark() - self.client.post(reverse('bookmarks:index.action'), { - 'remove': [bookmark.id], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "remove": [bookmark.id], + }, + ) self.assertEqual(Bookmark.objects.count(), 0) 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) - response = self.client.post(reverse('bookmarks:index.action'), { - 'remove': [bookmark.id], - }) + response = self.client.post( + reverse("bookmarks:index.action"), + { + "remove": [bookmark.id], + }, + ) self.assertEqual(response.status_code, 404) self.assertTrue(Bookmark.objects.filter(id=bookmark.id).exists()) def test_mark_as_read(self): bookmark = self.setup_bookmark(unread=True) - self.client.post(reverse('bookmarks:index.action'), { - 'mark_as_read': [bookmark.id], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "mark_as_read": [bookmark.id], + }, + ) bookmark.refresh_from_db() self.assertFalse(bookmark.unread) @@ -97,21 +127,29 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): def test_unshare_should_unshare_bookmark(self): bookmark = self.setup_bookmark(shared=True) - self.client.post(reverse('bookmarks:index.action'), { - 'unshare': [bookmark.id], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "unshare": [bookmark.id], + }, + ) bookmark.refresh_from_db() self.assertFalse(bookmark.shared) 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) - response = self.client.post(reverse('bookmarks:index.action'), { - 'unshare': [bookmark.id], - }) + response = self.client.post( + reverse("bookmarks:index.action"), + { + "unshare": [bookmark.id], + }, + ) bookmark.refresh_from_db() @@ -123,27 +161,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark() bookmark3 = self.setup_bookmark() - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_archive'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).is_archived) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived) 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) bookmark2 = self.setup_bookmark(user=other_user) bookmark3 = self.setup_bookmark(user=other_user) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_archive'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).is_archived) @@ -154,27 +208,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark(is_archived=True) bookmark3 = self.setup_bookmark(is_archived=True) - self.client.post(reverse('bookmarks:archived.action'), { - 'bulk_action': ['bulk_unarchive'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:archived.action"), + { + "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=bookmark2.id).is_archived) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived) 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) bookmark2 = 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'), { - 'bulk_action': ['bulk_unarchive'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:archived.action"), + { + "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=bookmark2.id).is_archived) @@ -185,27 +255,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark() bookmark3 = self.setup_bookmark() - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_delete'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).first()) self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first()) 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) bookmark2 = self.setup_bookmark(user=other_user) bookmark3 = self.setup_bookmark(user=other_user) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_delete'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).first()) @@ -218,12 +304,19 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): tag1 = self.setup_tag() tag2 = self.setup_tag() - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_tag'], - 'bulk_execute': [''], - 'bulk_tag_string': [f'{tag1.name} {tag2.name}'], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "bulk_action": ["bulk_tag"], + "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() bookmark2.refresh_from_db() @@ -234,19 +327,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2]) 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) bookmark2 = self.setup_bookmark(user=other_user) bookmark3 = self.setup_bookmark(user=other_user) tag1 = self.setup_tag() tag2 = self.setup_tag() - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_tag'], - 'bulk_execute': [''], - 'bulk_tag_string': [f'{tag1.name} {tag2.name}'], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "bulk_action": ["bulk_tag"], + "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() bookmark2.refresh_from_db() @@ -263,12 +365,19 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark(tags=[tag1, tag2]) bookmark3 = self.setup_bookmark(tags=[tag1, tag2]) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_untag'], - 'bulk_execute': [''], - 'bulk_tag_string': [f'{tag1.name} {tag2.name}'], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "bulk_action": ["bulk_untag"], + "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() bookmark2.refresh_from_db() @@ -279,19 +388,28 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): self.assertCountEqual(bookmark3.tags.all(), []) 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() tag2 = self.setup_tag() bookmark1 = 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) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_untag'], - 'bulk_execute': [''], - 'bulk_tag_string': [f'{tag1.name} {tag2.name}'], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "bulk_action": ["bulk_untag"], + "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() bookmark2.refresh_from_db() @@ -306,27 +424,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark(unread=True) bookmark3 = self.setup_bookmark(unread=True) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_read'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).unread) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).unread) 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) bookmark2 = self.setup_bookmark(unread=True, user=other_user) bookmark3 = self.setup_bookmark(unread=True, user=other_user) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_read'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).unread) @@ -337,27 +471,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark(unread=False) bookmark3 = self.setup_bookmark(unread=False) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_unread'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).unread) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).unread) 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) bookmark2 = self.setup_bookmark(unread=False, user=other_user) bookmark3 = self.setup_bookmark(unread=False, user=other_user) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_unread'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).unread) @@ -368,27 +518,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark(shared=False) bookmark3 = self.setup_bookmark(shared=False) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_share'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).shared) self.assertTrue(Bookmark.objects.get(id=bookmark3.id).shared) 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) bookmark2 = self.setup_bookmark(shared=False, user=other_user) bookmark3 = self.setup_bookmark(shared=False, user=other_user) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_share'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).shared) @@ -399,27 +565,43 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark(shared=True) bookmark3 = self.setup_bookmark(shared=True) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_unshare'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).shared) self.assertFalse(Bookmark.objects.get(id=bookmark3.id).shared) 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) bookmark2 = self.setup_bookmark(shared=True, user=other_user) bookmark3 = self.setup_bookmark(shared=True, user=other_user) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_unshare'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).shared) @@ -430,11 +612,14 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark() bookmark3 = self.setup_bookmark() - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_archive'], - 'bulk_execute': [''], - 'bulk_select_across': ['on'], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "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=bookmark2.id).is_archived) @@ -443,11 +628,14 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): def test_bulk_select_across_ignores_page(self): self.setup_numbered_bookmarks(100) - self.client.post(reverse('bookmarks:index.action') + '?page=2', { - 'bulk_action': ['bulk_delete'], - 'bulk_execute': [''], - 'bulk_select_across': ['on'], - }) + self.client.post( + reverse("bookmarks:index.action") + "?page=2", + { + "bulk_action": ["bulk_delete"], + "bulk_execute": [""], + "bulk_select_across": ["on"], + }, + ) self.assertEqual(0, Bookmark.objects.count()) @@ -455,85 +643,108 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): # 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, archived=True) - self.setup_numbered_bookmarks(3, - shared=True, - prefix="Joe's Bookmark", - user=self.setup_user(enable_sharing=True)) + self.setup_numbered_bookmarks( + 3, + shared=True, + prefix="Joe's Bookmark", + user=self.setup_user(enable_sharing=True), + ) def test_index_action_bulk_select_across_only_affects_active_bookmarks(self): self.setup_bulk_edit_scope_test_data() - 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 3').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 3").first()) - self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_delete'], - 'bulk_execute': [''], - 'bulk_select_across': ['on'], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "bulk_action": ["bulk_delete"], + "bulk_execute": [""], + "bulk_select_across": ["on"], + }, + ) self.assertEqual(6, Bookmark.objects.count()) - 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 3').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 3").first()) def test_index_action_bulk_select_across_respects_query(self): - self.setup_numbered_bookmarks(3, prefix='foo') - self.setup_numbered_bookmarks(3, prefix='bar') + self.setup_numbered_bookmarks(3, prefix="foo") + 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', { - 'bulk_action': ['bulk_delete'], - 'bulk_execute': [''], - 'bulk_select_across': ['on'], - }) + self.client.post( + reverse("bookmarks:index.action") + "?q=foo", + { + "bulk_action": ["bulk_delete"], + "bulk_execute": [""], + "bulk_select_across": ["on"], + }, + ) - self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count()) - self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count()) + self.assertEqual(0, Bookmark.objects.filter(title__startswith="foo").count()) + self.assertEqual(3, Bookmark.objects.filter(title__startswith="bar").count()) def test_archived_action_bulk_select_across_only_affects_archived_bookmarks(self): self.setup_bulk_edit_scope_test_data() - self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 1').first()) - self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 2').first()) - self.assertIsNotNone(Bookmark.objects.filter(title='Archived Bookmark 3').first()) + self.assertIsNotNone( + Bookmark.objects.filter(title="Archived Bookmark 1").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'), { - 'bulk_action': ['bulk_delete'], - 'bulk_execute': [''], - 'bulk_select_across': ['on'], - }) + self.client.post( + reverse("bookmarks:archived.action"), + { + "bulk_action": ["bulk_delete"], + "bulk_execute": [""], + "bulk_select_across": ["on"], + }, + ) self.assertEqual(6, Bookmark.objects.count()) - 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 3').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 3").first()) 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='bar', archived=True) + self.setup_numbered_bookmarks(3, prefix="foo", 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', { - 'bulk_action': ['bulk_delete'], - 'bulk_execute': [''], - 'bulk_select_across': ['on'], - }) + self.client.post( + reverse("bookmarks:archived.action") + "?q=foo", + { + "bulk_action": ["bulk_delete"], + "bulk_execute": [""], + "bulk_select_across": ["on"], + }, + ) - self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count()) - self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count()) + self.assertEqual(0, Bookmark.objects.filter(title__startswith="foo").count()) + self.assertEqual(3, Bookmark.objects.filter(title__startswith="bar").count()) def test_shared_action_bulk_select_across_not_supported(self): self.setup_bulk_edit_scope_test_data() - response = self.client.post(reverse('bookmarks:shared.action'), { - 'bulk_action': ['bulk_delete'], - 'bulk_execute': [''], - 'bulk_select_across': ['on'], - }) + response = self.client.post( + reverse("bookmarks:shared.action"), + { + "bulk_action": ["bulk_delete"], + "bulk_execute": [""], + "bulk_select_across": ["on"], + }, + ) self.assertEqual(response.status_code, 400) def test_handles_empty_bookmark_id(self): @@ -541,17 +752,23 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark() bookmark3 = self.setup_bookmark() - response = self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_archive'], - 'bulk_execute': [''], - }) + response = self.client.post( + reverse("bookmarks:index.action"), + { + "bulk_action": ["bulk_archive"], + "bulk_execute": [""], + }, + ) self.assertEqual(response.status_code, 302) - response = self.client.post(reverse('bookmarks:index.action'), { - 'bulk_action': ['bulk_archive'], - 'bulk_execute': [''], - 'bookmark_id': [], - }) + response = self.client.post( + reverse("bookmarks:index.action"), + { + "bulk_action": ["bulk_archive"], + "bulk_execute": [""], + "bookmark_id": [], + }, + ) self.assertEqual(response.status_code, 302) self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3]) @@ -561,9 +778,16 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark() bookmark3 = self.setup_bookmark() - self.client.post(reverse('bookmarks:index.action'), { - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + self.client.post( + reverse("bookmarks:index.action"), + { + "bookmark_id": [ + str(bookmark1.id), + str(bookmark2.id), + str(bookmark3.id), + ], + }, + ) self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3]) @@ -572,14 +796,25 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark2 = self.setup_bookmark() bookmark3 = self.setup_bookmark() - url = reverse('bookmarks:index.action') + '?return_url=' + reverse('bookmarks:settings.index') - response = self.client.post(url, { - 'bulk_action': ['bulk_archive'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }) + url = ( + reverse("bookmarks:index.action") + + "?return_url=" + + reverse("bookmarks:settings.index") + ) + 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): bookmark1 = self.setup_bookmark() @@ -587,19 +822,27 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin): bookmark3 = self.setup_bookmark() def post_with(return_url, follow=None): - url = reverse('bookmarks:index.action') + f'?return_url={return_url}' - return self.client.post(url, { - 'bulk_action': ['bulk_archive'], - 'bulk_execute': [''], - 'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)], - }, follow=follow) + url = reverse("bookmarks:index.action") + f"?return_url={return_url}" + return self.client.post( + url, + { + "bulk_action": ["bulk_archive"], + "bulk_execute": [""], + "bookmark_id": [ + str(bookmark1.id), + str(bookmark2.id), + str(bookmark3.id), + ], + }, + follow=follow, + ) - response = post_with('https://example.com') - self.assertRedirects(response, reverse('bookmarks:index')) - response = post_with('//example.com') - self.assertRedirects(response, reverse('bookmarks:index')) - response = post_with('://example.com') - self.assertRedirects(response, reverse('bookmarks:index')) + response = post_with("https://example.com") + self.assertRedirects(response, reverse("bookmarks:index")) + response = post_with("//example.com") + self.assertRedirects(response, reverse("bookmarks:index")) + response = post_with("://example.com") + 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) diff --git a/bookmarks/tests/test_bookmark_archived_view.py b/bookmarks/tests/test_bookmark_archived_view.py index 4702b7d..a082487 100644 --- a/bookmarks/tests/test_bookmark_archived_view.py +++ b/bookmarks/tests/test_bookmark_archived_view.py @@ -6,7 +6,11 @@ from django.test import TestCase from django.urls import reverse 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): @@ -15,33 +19,41 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin user = self.get_or_create_test_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()) - 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) - 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)) for bookmark in bookmarks: 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) - 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()) for bookmark in bookmarks: 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) def assertVisibleTags(self, response, tags: List[Tag]): 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) - 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)) 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]): 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] @@ -60,78 +72,103 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin def assertSelectedTags(self, response, tags: List[Tag]): 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) - tag_list = selected_tags.select('a') + tag_list = selected_tags.select("a") self.assertEqual(len(tag_list), len(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): html = response.content.decode() - self.assertInHTML(f''' + self.assertInHTML( + f""" Edit - ''', html) + """, + html, + ) def assertBulkActionForm(self, response, url: str): html = collapse_whitespace(response.content.decode()) - needle = collapse_whitespace(f''' + needle = collapse_whitespace( + f"""