Add black code formatter

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

15
Makefile Normal file
View file

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

View file

@ -281,6 +281,20 @@ python3 manage.py runserver
```
The frontend is now available under http://localhost:8000
### 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
```

View file

@ -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()

View file

@ -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")

View file

@ -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):

View file

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

View file

@ -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}

View file

@ -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="")

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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)
)

View file

@ -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)

View file

@ -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}"))

View file

@ -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")

View file

@ -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")

View file

@ -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"],
)

View file

@ -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)

View file

@ -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,
),
),
],
),
]

View file

@ -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"),
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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()],
),
),
]

View file

@ -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),
),
]

View file

@ -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),

View file

@ -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,
),
),
]

View file

@ -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),
),
]

View file

@ -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,
),
),
]

View file

@ -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,
),
),
]

View file

@ -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,
),
),
],
),
]

View file

@ -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 = [

View file

@ -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),

View file

@ -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,
),
),
],
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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,
),
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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):

View file

@ -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,
}

View file

@ -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):

View file

@ -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('<!DOCTYPE NETSCAPE-Bookmark-file-1>')
doc.append("<!DOCTYPE NETSCAPE-Bookmark-file-1>")
doc.append('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">')
doc.append('<TITLE>Bookmarks</TITLE>')
doc.append('<H1>Bookmarks</H1>')
doc.append("<TITLE>Bookmarks</TITLE>")
doc.append("<H1>Bookmarks</H1>")
def append_list_start(doc: BookmarkDocument):
doc.append('<DL><p>')
doc.append("<DL><p>")
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'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>')
f'<DT><A HREF="{url}" ADD_DATE="{added}" PRIVATE="{private}" TOREAD="{toread}" TAGS="{tags}">{title}</A>'
)
if desc:
doc.append(f'<DD>{desc}')
doc.append(f"<DD>{desc}")
def append_list_end(doc: BookmarkDocument):
doc.append('</DL><p>')
doc.append("</DL><p>")

View file

@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
# register mime type for .ico files, which is not included in the default
# 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

View file

@ -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)

View file

@ -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]:

View file

@ -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):

View file

@ -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)

View file

@ -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"

View file

@ -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 = '</head>'.encode('utf-8')
end_of_head = "</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():

View file

@ -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.

View file

@ -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,
}

View file

@ -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)

View file

@ -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)

View file

@ -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"""
<DT>
<A {f'HREF="{tag.href}"' if tag.href else ''}
{f'ADD_DATE="{tag.add_date}"' if tag.add_date else ''}
@ -211,13 +225,13 @@ class ImportTestMixin:
{tag.title if tag.title else ''}
</A>
{f'<DD>{tag.description}' if tag.description else ''}
'''
"""
def render_html(self, tags: List[BookmarkHtmlTag] = None, tags_html: str = ''):
def render_html(self, tags: List[BookmarkHtmlTag] = None, tags_html: str = ""):
if tags:
rendered_tags = [self.render_tag(tag) for tag in tags]
tags_html = '\n'.join(rendered_tags)
return f'''
tags_html = "\n".join(rendered_tags)
return f"""
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
@ -225,34 +239,34 @@ class ImportTestMixin:
<DL><p>
{tags_html}
</DL><p>
'''
"""
_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())

View file

@ -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'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
count=count)
url = reverse("bookmarks:shared")
self.assertContains(
response,
f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
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)

View file

@ -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"],
)

View file

@ -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")

View file

@ -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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

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