diff --git a/bookmarks/api/routes.py b/bookmarks/api/routes.py index 1f1005b..f114f07 100644 --- a/bookmarks/api/routes.py +++ b/bookmarks/api/routes.py @@ -6,7 +6,7 @@ from rest_framework.routers import DefaultRouter from bookmarks import queries from bookmarks.api.serializers import BookmarkSerializer, TagSerializer -from bookmarks.models import Bookmark, BookmarkFilters, Tag, User +from bookmarks.models import Bookmark, BookmarkSearch, Tag, User from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark, website_loader from bookmarks.services.website_loader import WebsiteMetadata @@ -34,8 +34,8 @@ class BookmarkViewSet(viewsets.GenericViewSet, user = self.request.user # For list action, use query set that applies search and tag projections if self.action == 'list': - query_string = self.request.GET.get('q') - return queries.query_bookmarks(user, user.profile, query_string) + search = BookmarkSearch.from_request(self.request) + return queries.query_bookmarks(user, user.profile, search) # For single entity actions use default query set without projections return Bookmark.objects.all().filter(owner=user) @@ -46,8 +46,8 @@ class BookmarkViewSet(viewsets.GenericViewSet, @action(methods=['get'], detail=False) def archived(self, request): user = request.user - query_string = request.GET.get('q') - query_set = queries.query_archived_bookmarks(user, user.profile, query_string) + search = BookmarkSearch.from_request(request) + query_set = queries.query_archived_bookmarks(user, user.profile, search) page = self.paginate_queryset(query_set) serializer = self.get_serializer_class() data = serializer(page, many=True).data @@ -55,10 +55,10 @@ class BookmarkViewSet(viewsets.GenericViewSet, @action(methods=['get'], detail=False) def shared(self, request): - filters = BookmarkFilters(request) - user = User.objects.filter(username=filters.user).first() + search = BookmarkSearch.from_request(request) + 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, filters.query, 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 diff --git a/bookmarks/context_processors.py b/bookmarks/context_processors.py index a179811..71a33c7 100644 --- a/bookmarks/context_processors.py +++ b/bookmarks/context_processors.py @@ -1,5 +1,5 @@ from bookmarks import queries -from bookmarks.models import Toast +from bookmarks.models import BookmarkSearch, Toast from bookmarks import utils @@ -17,7 +17,7 @@ def toasts(request): 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, '', 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, diff --git a/bookmarks/e2e/e2e_test_bookmark_page_partial_updates.py b/bookmarks/e2e/e2e_test_bookmark_page_partial_updates.py index a883c56..f218bb2 100644 --- a/bookmarks/e2e/e2e_test_bookmark_page_partial_updates.py +++ b/bookmarks/e2e/e2e_test_bookmark_page_partial_updates.py @@ -50,6 +50,21 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase): 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') + + with sync_playwright() as p: + 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.get_by_text('Archive').click() + + 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='-') diff --git a/bookmarks/feeds.py b/bookmarks/feeds.py index 3097315..92fdde6 100644 --- a/bookmarks/feeds.py +++ b/bookmarks/feeds.py @@ -4,7 +4,7 @@ from django.contrib.syndication.views import Feed from django.db.models import QuerySet from django.urls import reverse -from bookmarks.models import Bookmark, FeedToken +from bookmarks.models import Bookmark, BookmarkSearch, FeedToken from bookmarks import queries @@ -17,8 +17,8 @@ class FeedContext: class BaseBookmarksFeed(Feed): def get_object(self, request, feed_key: str): feed_token = FeedToken.objects.get(key__exact=feed_key) - query_string = request.GET.get('q') - query_set = queries.query_bookmarks(feed_token.user, feed_token.user.profile, query_string) + search = BookmarkSearch(query=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): diff --git a/bookmarks/frontend/api.js b/bookmarks/frontend/api.js index 9062116..dfe682f 100644 --- a/bookmarks/frontend/api.js +++ b/bookmarks/frontend/api.js @@ -3,10 +3,10 @@ export class ApiClient { this.baseUrl = baseUrl; } - listBookmarks(filters, options = { limit: 100, offset: 0, path: "" }) { + listBookmarks(search, options = { limit: 100, offset: 0, path: "" }) { const query = [`limit=${options.limit}`, `offset=${options.offset}`]; - Object.keys(filters).forEach((key) => { - const value = filters[key]; + Object.keys(search).forEach((key) => { + const value = search[key]; if (value) { query.push(`${key}=${encodeURIComponent(value)}`); } diff --git a/bookmarks/frontend/components/SearchAutoComplete.svelte b/bookmarks/frontend/components/SearchAutoComplete.svelte index e96a9e1..94f4e33 100644 --- a/bookmarks/frontend/components/SearchAutoComplete.svelte +++ b/bookmarks/frontend/components/SearchAutoComplete.svelte @@ -10,7 +10,7 @@ export let tags; export let mode = ''; export let apiClient; - export let filters; + export let search; export let linkTarget = '_blank'; let isFocus = false; @@ -115,11 +115,11 @@ if (value && value.length >= 3) { const path = mode ? `/${mode}` : '' - const suggestionFilters = { - ...filters, + const suggestionSearch = { + ...search, q: value } - const fetchedBookmarks = await apiClient.listBookmarks(suggestionFilters, {limit: 5, offset: 0, path}) + const fetchedBookmarks = await apiClient.listBookmarks(suggestionSearch, {limit: 5, offset: 0, path}) bookmarks = fetchedBookmarks.map(bookmark => { const fullLabel = bookmark.title || bookmark.website_title || bookmark.url const label = clampText(fullLabel, 60) diff --git a/bookmarks/management/commands/enable_wal.py b/bookmarks/management/commands/enable_wal.py index 91833e2..6c66e3d 100644 --- a/bookmarks/management/commands/enable_wal.py +++ b/bookmarks/management/commands/enable_wal.py @@ -11,7 +11,7 @@ class Command(BaseCommand): help = "Enable WAL journal mode when using an SQLite database" def handle(self, *args, **options): - if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3': + if not settings.USE_SQLITE: return connection = connections['default'] diff --git a/bookmarks/models.py b/bookmarks/models.py index 5023b29..e563d4e 100644 --- a/bookmarks/models.py +++ b/bookmarks/models.py @@ -124,10 +124,86 @@ class BookmarkForm(forms.ModelForm): return self.instance and self.instance.notes -class BookmarkFilters: - def __init__(self, request: WSGIRequest): - self.query = request.GET.get('q') or '' - self.user = request.GET.get('user') or '' +class BookmarkSearch: + SORT_ADDED_ASC = 'added_asc' + SORT_ADDED_DESC = 'added_desc' + SORT_TITLE_ASC = 'title_asc' + SORT_TITLE_DESC = 'title_desc' + + params = ['q', 'user', 'sort'] + defaults = { + 'q': '', + 'user': '', + 'sort': SORT_ADDED_DESC, + } + + def __init__(self, + q: str = defaults['q'], + query: str = defaults['q'], # alias for q + user: str = defaults['user'], + sort: str = defaults['sort']): + self.q = q or query + self.user = user + self.sort = sort + + @property + def query(self): + return self.q + + def is_modified(self, param): + value = self.__dict__[param] + return value and value != BookmarkSearch.defaults[param] + + @property + def modified_params(self): + return [field for field in self.params if self.is_modified(field)] + + @property + def query_params(self): + return {param: self.__dict__[param] for param in self.modified_params} + + @staticmethod + def from_request(request: WSGIRequest): + initial_values = {} + for param in BookmarkSearch.params: + value = request.GET.get(param) + if value: + initial_values[param] = value + + return BookmarkSearch(**initial_values) + + +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 ↓'), + ] + + q = forms.CharField() + user = forms.ChoiceField() + sort = forms.ChoiceField(choices=SORT_CHOICES) + + def __init__(self, search: BookmarkSearch, editable_fields: List[str] = None, users: List[User] = None): + super().__init__() + editable_fields = editable_fields or [] + + # 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 + + for param in search.params: + # set initial values for modified params + self.fields[param].initial = search.__dict__[param] + + # Mark non-editable modified fields as hidden. That way, templates + # rendering a form can just loop over hidden_fields to ensure that + # all necessary search options are kept when submitting the form. + if search.is_modified(param) and param not in editable_fields: + self.fields[param].widget = forms.HiddenInput() class UserProfile(models.Model): diff --git a/bookmarks/queries.py b/bookmarks/queries.py index 6af1b5c..398db80 100644 --- a/bookmarks/queries.py +++ b/bookmarks/queries.py @@ -1,32 +1,35 @@ from typing import Optional +from django.conf import settings from django.contrib.auth.models import User -from django.db.models import Q, QuerySet, Exists, OuterRef +from django.db.models import Q, QuerySet, Exists, OuterRef, Case, When, CharField +from django.db.models.expressions import RawSQL +from django.db.models.functions import Lower -from bookmarks.models import Bookmark, Tag, UserProfile +from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile from bookmarks.utils import unique -def query_bookmarks(user: User, profile: UserProfile, query_string: str) -> QuerySet: - return _base_bookmarks_query(user, profile, query_string) \ +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, query_string: str) -> QuerySet: - return _base_bookmarks_query(user, profile, query_string) \ +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, query_string: str, +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) - return _base_bookmarks_query(user, profile, query_string).filter(conditions) + return _base_bookmarks_query(user, profile, search).filter(conditions) -def _base_bookmarks_query(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet: +def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: BookmarkSearch) -> QuerySet: query_set = Bookmark.objects # Filter for user @@ -34,7 +37,7 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, query_stri query_set = query_set.filter(owner=user) # Split query into search terms and tags - query = parse_query_string(query_string) + query = parse_query_string(search.query) # Filter for search terms and tags for term in query['search_terms']: @@ -67,38 +70,66 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, query_stri ) # Sort by date added - query_set = query_set.order_by('-date_added') + if search.sort == BookmarkSearch.SORT_ADDED_ASC: + query_set = query_set.order_by('date_added') + elif search.sort == BookmarkSearch.SORT_ADDED_DESC: + 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: + # 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() + )) + + # 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', ()) + else: + order_field = 'effective_title' + + if search.sort == BookmarkSearch.SORT_TITLE_ASC: + query_set = query_set.order_by(order_field) + elif search.sort == BookmarkSearch.SORT_TITLE_DESC: + query_set = query_set.order_by(order_field).reverse() return query_set -def query_bookmark_tags(user: User, profile: UserProfile, query_string: str) -> QuerySet: - bookmarks_query = query_bookmarks(user, profile, query_string) +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) return query_set.distinct() -def query_archived_bookmark_tags(user: User, profile: UserProfile, query_string: str) -> QuerySet: - bookmarks_query = query_archived_bookmarks(user, profile, query_string) +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) return query_set.distinct() -def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, query_string: str, +def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, search: BookmarkSearch, public_only: bool) -> QuerySet: - bookmarks_query = query_shared_bookmarks(user, profile, query_string, public_only) + bookmarks_query = query_shared_bookmarks(user, profile, search, public_only) query_set = Tag.objects.filter(bookmark__in=bookmarks_query) return query_set.distinct() -def query_shared_bookmark_users(profile: UserProfile, query_string: str, public_only: bool) -> QuerySet: - bookmarks_query = query_shared_bookmarks(None, profile, query_string, public_only) +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) diff --git a/bookmarks/signals.py b/bookmarks/signals.py index 915352a..0fd3b15 100644 --- a/bookmarks/signals.py +++ b/bookmarks/signals.py @@ -1,14 +1,10 @@ -from os import path - +from django.conf import settings from django.contrib.auth import user_logged_in from django.db.backends.signals import connection_created from django.dispatch import receiver from bookmarks.services import tasks -icu_extension_path = './libicu.so' -icu_extension_exists = path.exists(icu_extension_path) - @receiver(user_logged_in) def user_logged_in(sender, request, user, **kwargs): @@ -19,9 +15,9 @@ 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 icu_extension_exists: + if connection.vendor == 'sqlite' and settings.USE_SQLITE_ICU_EXTENSION: connection.connection.enable_load_extension(True) - connection.connection.load_extension('./libicu') + connection.connection.load_extension(settings.SQLITE_ICU_EXTENSION_PATH.rstrip('.so')) with connection.cursor() as cursor: # Load an ICU collation for case-insensitive ordering. diff --git a/bookmarks/styles/bookmark-page.scss b/bookmarks/styles/bookmark-page.scss index cdba178..3f4aef6 100644 --- a/bookmarks/styles/bookmark-page.scss +++ b/bookmarks/styles/bookmark-page.scss @@ -40,6 +40,41 @@ } } } + + // Group search options button with search button + .input-group input[type='submit'] { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .dropdown button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + .dropdown { + margin-left: -1px; + } + + // Search option menu styles + .dropdown { + .menu { + padding: $unit-4; + min-width: 220px; + } + + &:focus-within { + .menu { + display: block; + } + } + + .menu .actions { + margin-top: $unit-4; + display: flex; + justify-content: space-between; + } + } } /* Bookmark list */ diff --git a/bookmarks/templates/bookmarks/archive.html b/bookmarks/templates/bookmarks/archive.html index 80eaed4..dae54fa 100644 --- a/bookmarks/templates/bookmarks/archive.html +++ b/bookmarks/templates/bookmarks/archive.html @@ -15,13 +15,14 @@

Archived bookmarks

- {% bookmark_search bookmark_list.filters tag_cloud.tags mode='archived' %} + {% bookmark_search bookmark_list.search tag_cloud.tags mode='archived' %} {% include 'bookmarks/bulk_edit/toggle.html' %}
-
+ {% csrf_token %} {% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_archive' %} diff --git a/bookmarks/templates/bookmarks/bookmark_list.html b/bookmarks/templates/bookmarks/bookmark_list.html index 55ad673..5b44843 100644 --- a/bookmarks/templates/bookmarks/bookmark_list.html +++ b/bookmarks/templates/bookmarks/bookmark_list.html @@ -65,7 +65,7 @@ {% endif %} {% if bookmark_item.is_editable %} {# Bookmark owner actions #} - Edit + Edit {% if bookmark_item.is_archived %} + + - {% if filters.user %} - - {% endif %} + + {% for hidden_field in form.hidden_fields %} + {{ hidden_field }} + {% endfor %}
@@ -19,9 +50,9 @@ const currentTagsString = '{{ tags_string }}'; const currentTags = currentTagsString.split(' '); const uniqueTags = [...new Set(currentTags)] - const filters = { - q: '{{ filters.query }}', - user: '{{ filters.user }}', + const search = { + q: '{{ search.query }}', + user: '{{ search.user }}', } const apiClient = new linkding.ApiClient('{% url 'bookmarks:api-root' %}') const wrapper = document.getElementById('search-input-wrap') @@ -31,12 +62,12 @@ props: { name: 'q', placeholder: 'Search for words or #tags', - value: '{{ filters.query }}', + value: '{{ search.query|safe }}', tags: uniqueTags, mode: '{{ mode }}', linkTarget: '{{ request.user_profile.bookmark_link_target }}', apiClient, - filters, + search, } }) wrapper.parentElement.replaceChild(newWrapper, wrapper) diff --git a/bookmarks/templates/bookmarks/shared.html b/bookmarks/templates/bookmarks/shared.html index 7332aaa..d424023 100644 --- a/bookmarks/templates/bookmarks/shared.html +++ b/bookmarks/templates/bookmarks/shared.html @@ -13,10 +13,10 @@

Shared bookmarks

- {% bookmark_search bookmark_list.filters tag_cloud.tags mode='shared' %} + {% bookmark_search bookmark_list.search tag_cloud.tags mode='shared' %}
-
{% csrf_token %} @@ -32,7 +32,7 @@

User

- {% user_select filters users %} + {% user_select bookmark_list.search users %}
diff --git a/bookmarks/templates/bookmarks/user_select.html b/bookmarks/templates/bookmarks/user_select.html index 1d1a46b..1286702 100644 --- a/bookmarks/templates/bookmarks/user_select.html +++ b/bookmarks/templates/bookmarks/user_select.html @@ -1,19 +1,12 @@ +{% load widget_tweaks %} + - {% if filters.query %} - - {% endif %} + {% for hidden_field in form.hidden_fields %} + {{ hidden_field }} + {% endfor %}
- + {{ form.user|add_class:"form-select" }} diff --git a/bookmarks/templatetags/bookmarks.py b/bookmarks/templatetags/bookmarks.py index 271d5b8..c08fb16 100644 --- a/bookmarks/templatetags/bookmarks.py +++ b/bookmarks/templatetags/bookmarks.py @@ -2,7 +2,7 @@ from typing import List from django import template -from bookmarks.models import BookmarkForm, BookmarkFilters, Tag, build_tag_string, User +from bookmarks.models import BookmarkForm, BookmarkSearch, BookmarkSearchForm, Tag, build_tag_string, User register = template.Library() @@ -19,21 +19,25 @@ def bookmark_form(context, form: BookmarkForm, cancel_url: str, bookmark_id: int @register.inclusion_tag('bookmarks/search.html', name='bookmark_search', takes_context=True) -def bookmark_search(context, filters: BookmarkFilters, tags: [Tag], mode: str = ''): +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, ' ') + form = BookmarkSearchForm(search, editable_fields=['q', 'sort']) return { 'request': context['request'], - 'filters': filters, + 'search': search, + 'form': form, 'tags_string': tags_string, 'mode': mode, } @register.inclusion_tag('bookmarks/user_select.html', name='user_select', takes_context=True) -def user_select(context, filters: BookmarkFilters, users: List[User]): +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) return { - 'filters': filters, + 'search': search, 'users': sorted_users, + 'form': form, } diff --git a/bookmarks/tests/helpers.py b/bookmarks/tests/helpers.py index d672546..f52d41c 100644 --- a/bookmarks/tests/helpers.py +++ b/bookmarks/tests/helpers.py @@ -29,7 +29,7 @@ class BookmarkFactoryMixin: tags=None, user: User = None, url: str = '', - title: str = '', + title: str = None, description: str = '', notes: str = '', website_title: str = '', @@ -38,7 +38,7 @@ class BookmarkFactoryMixin: favicon_file: str = '', added: datetime = None, ): - if not title: + if title is None: title = get_random_string(length=32) if tags is None: tags = [] @@ -81,6 +81,7 @@ class BookmarkFactoryMixin: with_tags: bool = False, user: User = None): user = user or self.get_or_create_test_user() + bookmarks = [] if not prefix: if archived: @@ -105,7 +106,11 @@ class BookmarkFactoryMixin: if with_tags: tag_name = f'{tag_prefix} {i}{suffix}' tags = [self.setup_tag(name=tag_name)] - self.setup_bookmark(url=url, title=title, is_archived=archived, shared=shared, tags=tags, user=user) + bookmark = self.setup_bookmark(url=url, title=title, is_archived=archived, shared=shared, tags=tags, + user=user) + bookmarks.append(bookmark) + + return bookmarks def get_numbered_bookmark(self, title: str): return Bookmark.objects.get(title=title) @@ -128,6 +133,9 @@ class BookmarkFactoryMixin: user.profile.save() return user + def get_random_string(self, length: int = 32): + return get_random_string(length=length) + class HtmlTestMixin: def make_soup(self, html: str): diff --git a/bookmarks/tests/test_bookmark_archived_view.py b/bookmarks/tests/test_bookmark_archived_view.py index 1ba10d7..ca7a6f3 100644 --- a/bookmarks/tests/test_bookmark_archived_view.py +++ b/bookmarks/tests/test_bookmark_archived_view.py @@ -1,3 +1,4 @@ +import urllib.parse from typing import List from django.contrib.auth.models import User @@ -55,6 +56,21 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin for tag in tags: 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''' + Edit + ''', html) + + def assertBulkActionForm(self, response, url: str): + html = collapse_whitespace(response.content.decode()) + needle = collapse_whitespace(f''' + + ''') + 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') visible_bookmarks = [ @@ -219,6 +235,61 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin 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') + + # without query params + return_url = urllib.parse.quote(base_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' + return_url = urllib.parse.quote(base_url + url_params) + 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' + return_url = urllib.parse.quote(base_url + url_params) + 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') + + # without params + return_url = urllib.parse.quote_plus(base_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' + return_url = urllib.parse.quote_plus(base_url + url_params) + 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' + return_url = urllib.parse.quote_plus(base_url + url_params) + 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') response = self.client.get(url) diff --git a/bookmarks/tests/test_bookmark_index_view.py b/bookmarks/tests/test_bookmark_index_view.py index 1185676..8810669 100644 --- a/bookmarks/tests/test_bookmark_index_view.py +++ b/bookmarks/tests/test_bookmark_index_view.py @@ -1,5 +1,5 @@ -from typing import List import urllib.parse +from typing import List from django.contrib.auth.models import User from django.test import TestCase @@ -56,6 +56,21 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin): for tag in tags: 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''' + Edit + ''', html) + + def assertBulkActionForm(self, response, url: str): + html = collapse_whitespace(response.content.decode()) + needle = collapse_whitespace(f''' + + ''') + 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') visible_bookmarks = [ @@ -220,30 +235,60 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin): self.assertVisibleBookmarks(response, visible_bookmarks, '_self') - def test_edit_link_return_url_should_contain_query_params(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') # without query params - url = reverse('bookmarks:index') - response = self.client.get(url) - html = response.content.decode() - edit_url = reverse('bookmarks:edit', args=[bookmark.id]) - return_url = urllib.parse.quote_plus(url) + return_url = urllib.parse.quote(base_url) + url = f'{edit_url}?return_url={return_url}' - self.assertInHTML(f''' - Edit - ''', html) + response = self.client.get(base_url) + self.assertEditLink(response, url) - # with query params - url = reverse('bookmarks:index') + '?q=foo&user=user' - response = self.client.get(url) - html = response.content.decode() - edit_url = reverse('bookmarks:edit', args=[bookmark.id]) - return_url = urllib.parse.quote_plus(url) + # with query + url_params = '?q=foo' + return_url = urllib.parse.quote(base_url + url_params) + url = f'{edit_url}?return_url={return_url}' - self.assertInHTML(f''' - Edit - ''', html) + 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' + return_url = urllib.parse.quote(base_url + url_params) + 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') + + # without params + return_url = urllib.parse.quote_plus(base_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' + return_url = urllib.parse.quote_plus(base_url + url_params) + 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' + return_url = urllib.parse.quote_plus(base_url + url_params) + 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') diff --git a/bookmarks/tests/test_bookmark_search_form.py b/bookmarks/tests/test_bookmark_search_form.py new file mode 100644 index 0000000..468b506 --- /dev/null +++ b/bookmarks/tests/test_bookmark_search_form.py @@ -0,0 +1,58 @@ +from django.test import TestCase + +from bookmarks.models import BookmarkSearch, BookmarkSearchForm +from bookmarks.tests.helpers import BookmarkFactoryMixin + + +class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin): + def test_initial_values(self): + # no params + search = BookmarkSearch() + form = BookmarkSearchForm(search) + self.assertEqual(form['q'].initial, '') + self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_DESC) + self.assertEqual(form['user'].initial, '') + + # with params + search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123') + form = BookmarkSearchForm(search) + self.assertEqual(form['q'].initial, 'search query') + self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_ASC) + self.assertEqual(form['user'].initial, 'user123') + + def test_user_options(self): + users = [ + 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'), + ]) + + def test_hidden_fields(self): + # no modified params + search = BookmarkSearch() + form = BookmarkSearchForm(search) + self.assertEqual(len(form.hidden_fields()), 0) + + # some modified params + search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC) + form = BookmarkSearchForm(search) + self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort']]) + + # all modified params + search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123') + form = BookmarkSearchForm(search) + self.assertCountEqual(form.hidden_fields(), [form['q'], form['sort'], form['user']]) + + # 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']]) diff --git a/bookmarks/tests/test_bookmark_search_model.py b/bookmarks/tests/test_bookmark_search_model.py new file mode 100644 index 0000000..54699b4 --- /dev/null +++ b/bookmarks/tests/test_bookmark_search_model.py @@ -0,0 +1,59 @@ +from unittest.mock import Mock +from bookmarks.models import BookmarkSearch +from django.test import TestCase + + +class BookmarkSearchModelTest(TestCase): + def test_from_request(self): + # no params + mock_request = Mock() + mock_request.GET = {} + + search = BookmarkSearch.from_request(mock_request) + self.assertEqual(search.q, '') + self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC) + self.assertEqual(search.user, '') + + # some params + mock_request.GET = { + 'q': 'search query', + 'user': 'user123', + } + + bookmark_search = BookmarkSearch.from_request(mock_request) + self.assertEqual(bookmark_search.q, 'search query') + self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC) + self.assertEqual(bookmark_search.user, 'user123') + + # all params + mock_request.GET = { + 'q': 'search query', + 'user': 'user123', + 'sort': BookmarkSearch.SORT_TITLE_ASC + } + + search = BookmarkSearch.from_request(mock_request) + self.assertEqual(search.q, 'search query') + self.assertEqual(search.user, 'user123') + self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC) + + def test_modified_params(self): + # no params + bookmark_search = BookmarkSearch() + modified_params = bookmark_search.modified_params + self.assertEqual(len(modified_params), 0) + + # params are default values + bookmark_search = BookmarkSearch(q='', sort=BookmarkSearch.SORT_ADDED_DESC, user='') + 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) + modified_params = bookmark_search.modified_params + self.assertCountEqual(modified_params, ['q', 'sort']) + + # all modified params + bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC, user='user123') + modified_params = bookmark_search.modified_params + self.assertCountEqual(modified_params, ['q', 'sort', 'user']) diff --git a/bookmarks/tests/test_bookmark_search_tag.py b/bookmarks/tests/test_bookmark_search_tag.py index bcb5de9..e75e807 100644 --- a/bookmarks/tests/test_bookmark_search_tag.py +++ b/bookmarks/tests/test_bookmark_search_tag.py @@ -2,7 +2,7 @@ from django.db.models import QuerySet from django.template import Template, RequestContext from django.test import TestCase, RequestFactory -from bookmarks.models import BookmarkFilters, Tag +from bookmarks.models import BookmarkSearch, Tag from bookmarks.tests.helpers import BookmarkFactoryMixin @@ -12,31 +12,43 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin): request = rf.get(url) request.user = self.get_or_create_test_user() request.user_profile = self.get_or_create_test_user().profile - filters = BookmarkFilters(request) + search = BookmarkSearch.from_request(request) context = RequestContext(request, { 'request': request, - 'filters': filters, + 'search': search, 'tags': tags, }) template_to_render = Template( '{% load bookmarks %}' - '{% bookmark_search filters tags %}' + '{% bookmark_search search tags %}' ) return template_to_render.render(context) - def test_render_hidden_inputs_for_filter_params(self): - # Should render hidden inputs if query param exists - url = '/test?q=foo&user=john' + def assertHiddenInput(self, html: str, name: str, value: str = None): + needle = f' - ''', rendered_template) + self.assertNoHiddenInput(rendered_template, 'user') + self.assertNoHiddenInput(rendered_template, 'q') + self.assertNoHiddenInput(rendered_template, 'sort') - # Should not render hidden inputs if query param does not exist - url = '/test?q=foo' + # With params + url = '/test?q=foo&user=john&sort=title_asc' rendered_template = self.render_template(url) - self.assertInHTML(''' - - ''', rendered_template, count=0) + self.assertHiddenInput(rendered_template, 'user', 'john') + self.assertNoHiddenInput(rendered_template, 'q') + self.assertNoHiddenInput(rendered_template, 'sort') diff --git a/bookmarks/tests/test_bookmark_shared_view.py b/bookmarks/tests/test_bookmark_shared_view.py index 76e7224..955bbb8 100644 --- a/bookmarks/tests/test_bookmark_shared_view.py +++ b/bookmarks/tests/test_bookmark_shared_view.py @@ -1,3 +1,4 @@ +import urllib.parse from typing import List from django.contrib.auth.models import User @@ -45,24 +46,25 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin): def assertVisibleUserOptions(self, response, users: List[User]): html = response.content.decode() - self.assertContains(response, 'data-is-user-option', count=len(users)) + user_options = [ + '' + ] for user in users: - self.assertInHTML(f''' - - ''', html) + user_options.append(f'') + user_select_html = f''' + + ''' - def assertInvisibleUserOptions(self, response, users: List[User]): + self.assertInHTML(user_select_html, html) + + def assertEditLink(self, response, url): html = response.content.decode() - - for user in users: - self.assertInHTML(f''' - - ''', html, count=0) + self.assertInHTML(f''' + Edit + ''', html) def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(self): self.authenticate() @@ -267,41 +269,33 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin): def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self): self.authenticate() expected_visible_users = [ - self.setup_user(enable_sharing=True), - self.setup_user(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]) - expected_invisible_users = [ - self.setup_user(enable_sharing=True), - self.setup_user(enable_sharing=False), - ] - self.setup_bookmark(shared=False, user=expected_invisible_users[0]) - self.setup_bookmark(shared=True, user=expected_invisible_users[1]) + 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')) self.assertVisibleUserOptions(response, expected_visible_users) - self.assertInvisibleUserOptions(response, expected_invisible_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(enable_sharing=True, enable_public_sharing=True), - self.setup_user(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]) - expected_invisible_users = [ - self.setup_user(enable_sharing=True), - self.setup_user(enable_sharing=True), - ] - self.setup_bookmark(shared=True, user=expected_invisible_users[0]) - self.setup_bookmark(shared=True, user=expected_invisible_users[1]) + # users with public sharing disabled + 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')) self.assertVisibleUserOptions(response, expected_visible_users) - self.assertInvisibleUserOptions(response, expected_invisible_users) def test_should_open_bookmarks_in_new_page_by_default(self): self.authenticate() @@ -334,3 +328,44 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin): response = self.client.get(reverse('bookmarks:shared')) self.assertVisibleBookmarks(response, visible_bookmarks, '_self') + + def test_edit_link_return_url_respects_search_options(self): + self.authenticate() + user = self.get_or_create_test_user() + 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') + + # without query params + return_url = urllib.parse.quote(base_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' + return_url = urllib.parse.quote(base_url + url_params) + 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}' + return_url = urllib.parse.quote(base_url + url_params) + 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' + return_url = urllib.parse.quote(base_url + url_params) + url = f'{edit_url}?return_url={return_url}' + + response = self.client.get(base_url + url_params) + self.assertEditLink(response, url) diff --git a/bookmarks/tests/test_bookmarks_api.py b/bookmarks/tests/test_bookmarks_api.py index fa4c73c..176bf0d 100644 --- a/bookmarks/tests/test_bookmarks_api.py +++ b/bookmarks/tests/test_bookmarks_api.py @@ -15,15 +15,6 @@ from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): - def setUp(self) -> None: - self.tag1 = self.setup_tag() - self.tag2 = self.setup_tag() - self.bookmark1 = self.setup_bookmark(tags=[self.tag1, self.tag2], notes='Test notes') - self.bookmark2 = self.setup_bookmark() - self.bookmark3 = self.setup_bookmark(tags=[self.tag2]) - self.archived_bookmark1 = self.setup_bookmark(is_archived=True, tags=[self.tag1, self.tag2]) - self.archived_bookmark2 = self.setup_bookmark(is_archived=True) - def authenticate(self): 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) @@ -56,29 +47,64 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): def test_list_bookmarks(self): self.authenticate() + bookmarks = self.setup_numbered_bookmarks(5) response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK) - self.assertBookmarkListEqual(response.data['results'], [self.bookmark1, self.bookmark2, self.bookmark3]) + self.assertBookmarkListEqual(response.data['results'], bookmarks) + + def test_list_bookmarks_does_not_return_archived_bookmarks(self): + self.authenticate() + bookmarks = self.setup_numbered_bookmarks(5) + self.setup_numbered_bookmarks(5, archived=True) + + response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual(response.data['results'], bookmarks) def test_list_bookmarks_should_filter_by_query(self): self.authenticate() + search_value = self.get_random_string() + bookmarks = self.setup_numbered_bookmarks(5, prefix=search_value) + self.setup_numbered_bookmarks(5) - response = self.get(reverse('bookmarks:bookmark-list') + '?q=#' + self.tag1.name, + response = self.get(reverse('bookmarks:bookmark-list') + '?q=' + search_value, expected_status_code=status.HTTP_200_OK) - self.assertBookmarkListEqual(response.data['results'], [self.bookmark1]) + self.assertBookmarkListEqual(response.data['results'], bookmarks) + + def test_list_bookmarks_should_respect_sort(self): + self.authenticate() + bookmarks = self.setup_numbered_bookmarks(5) + bookmarks.reverse() + + response = self.get(reverse('bookmarks:bookmark-list') + '?sort=title_desc', + expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual(response.data['results'], bookmarks) def test_list_archived_bookmarks_does_not_return_unarchived_bookmarks(self): self.authenticate() + self.setup_numbered_bookmarks(5) + archived_bookmarks = self.setup_numbered_bookmarks(5, archived=True) response = self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK) - self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1, self.archived_bookmark2]) + self.assertBookmarkListEqual(response.data['results'], archived_bookmarks) def test_list_archived_bookmarks_should_filter_by_query(self): self.authenticate() + search_value = self.get_random_string() + archived_bookmarks = self.setup_numbered_bookmarks(5, archived=True, prefix=search_value) + self.setup_numbered_bookmarks(5, archived=True) - response = self.get(reverse('bookmarks:bookmark-archived') + '?q=#' + self.tag1.name, + response = self.get(reverse('bookmarks:bookmark-archived') + '?q=' + search_value, expected_status_code=status.HTTP_200_OK) - self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1]) + self.assertBookmarkListEqual(response.data['results'], archived_bookmarks) + + def test_list_archived_bookmarks_should_respect_sort(self): + self.authenticate() + bookmarks = self.setup_numbered_bookmarks(5, archived=True) + bookmarks.reverse() + + response = self.get(reverse('bookmarks:bookmark-archived') + '?sort=title_desc', + expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual(response.data['results'], bookmarks) def test_list_shared_bookmarks(self): self.authenticate() @@ -158,6 +184,16 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): expected_status_code=status.HTTP_200_OK) self.assertBookmarkListEqual(response.data['results'], expected_bookmarks) + def test_list_shared_bookmarks_should_respect_sort(self): + self.authenticate() + user = self.setup_user(enable_sharing=True) + bookmarks = self.setup_numbered_bookmarks(5, shared=True, user=user) + bookmarks.reverse() + + response = self.get(reverse('bookmarks:bookmark-shared') + '?sort=title_desc', + expected_status_code=status.HTTP_200_OK) + self.assertBookmarkListEqual(response.data['results'], bookmarks) + def test_create_bookmark(self): self.authenticate() @@ -295,34 +331,38 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): def test_get_bookmark(self): self.authenticate() + bookmark = self.setup_bookmark() - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) response = self.get(url, expected_status_code=status.HTTP_200_OK) - self.assertBookmarkListEqual([response.data], [self.bookmark1]) + self.assertBookmarkListEqual([response.data], [bookmark]) def test_update_bookmark(self): self.authenticate() + bookmark = self.setup_bookmark() - data = {'url': 'https://example.com/'} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + data = {'url': 'https://example.com/updated'} + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.put(url, data, expected_status_code=status.HTTP_200_OK) - updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id) + updated_bookmark = Bookmark.objects.get(id=bookmark.id) self.assertEqual(updated_bookmark.url, data['url']) def test_update_bookmark_fails_without_required_fields(self): self.authenticate() + bookmark = self.setup_bookmark() data = {'title': 'https://example.com/'} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST) def test_update_bookmark_with_minimal_payload_clears_all_fields(self): self.authenticate() + bookmark = self.setup_bookmark() data = {'url': 'https://example.com/'} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.put(url, data, expected_status_code=status.HTTP_200_OK) - updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id) + updated_bookmark = Bookmark.objects.get(id=bookmark.id) self.assertEqual(updated_bookmark.url, data['url']) self.assertEqual(updated_bookmark.title, '') self.assertEqual(updated_bookmark.description, '') @@ -331,112 +371,119 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): def test_update_bookmark_unread_flag(self): self.authenticate() + bookmark = self.setup_bookmark() data = {'url': 'https://example.com/', 'unread': True} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.put(url, data, expected_status_code=status.HTTP_200_OK) - updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id) + updated_bookmark = Bookmark.objects.get(id=bookmark.id) self.assertEqual(updated_bookmark.unread, True) def test_update_bookmark_shared_flag(self): self.authenticate() + bookmark = self.setup_bookmark() data = {'url': 'https://example.com/', 'shared': True} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.put(url, data, expected_status_code=status.HTTP_200_OK) - updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id) + updated_bookmark = Bookmark.objects.get(id=bookmark.id) self.assertEqual(updated_bookmark.shared, True) def test_patch_bookmark(self): self.authenticate() + bookmark = self.setup_bookmark() data = {'url': 'https://example.com'} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - self.assertEqual(self.bookmark1.url, data['url']) + bookmark.refresh_from_db() + self.assertEqual(bookmark.url, data['url']) data = {'title': 'Updated title'} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - self.assertEqual(self.bookmark1.title, data['title']) + bookmark.refresh_from_db() + self.assertEqual(bookmark.title, data['title']) data = {'description': 'Updated description'} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - self.assertEqual(self.bookmark1.description, data['description']) + bookmark.refresh_from_db() + self.assertEqual(bookmark.description, data['description']) data = {'notes': 'Updated notes'} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - self.assertEqual(self.bookmark1.notes, data['notes']) + bookmark.refresh_from_db() + self.assertEqual(bookmark.notes, data['notes']) data = {'unread': True} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - self.assertTrue(self.bookmark1.unread) + bookmark.refresh_from_db() + self.assertTrue(bookmark.unread) data = {'unread': False} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - self.assertFalse(self.bookmark1.unread) + bookmark.refresh_from_db() + self.assertFalse(bookmark.unread) data = {'shared': True} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - self.assertTrue(self.bookmark1.shared) + bookmark.refresh_from_db() + self.assertTrue(bookmark.shared) data = {'shared': False} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - self.assertFalse(self.bookmark1.shared) + bookmark.refresh_from_db() + self.assertFalse(bookmark.shared) data = {'tag_names': ['updated-tag-1', 'updated-tag-2']} - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, data, expected_status_code=status.HTTP_200_OK) - self.bookmark1.refresh_from_db() - tag_names = [tag.name for tag in self.bookmark1.tags.all()] + bookmark.refresh_from_db() + tag_names = [tag.name for tag in bookmark.tags.all()] self.assertListEqual(tag_names, ['updated-tag-1', 'updated-tag-2']) def test_patch_with_empty_payload_does_not_modify_bookmark(self): self.authenticate() + bookmark = self.setup_bookmark() - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.patch(url, {}, expected_status_code=status.HTTP_200_OK) - updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id) - self.assertEqual(updated_bookmark.url, self.bookmark1.url) - self.assertEqual(updated_bookmark.title, self.bookmark1.title) - self.assertEqual(updated_bookmark.description, self.bookmark1.description) - self.assertListEqual(updated_bookmark.tag_names, self.bookmark1.tag_names) + updated_bookmark = Bookmark.objects.get(id=bookmark.id) + self.assertEqual(updated_bookmark.url, bookmark.url) + self.assertEqual(updated_bookmark.title, bookmark.title) + self.assertEqual(updated_bookmark.description, bookmark.description) + self.assertListEqual(updated_bookmark.tag_names, bookmark.tag_names) def test_delete_bookmark(self): self.authenticate() + bookmark = self.setup_bookmark() - url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-detail', args=[bookmark.id]) self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT) - self.assertEqual(len(Bookmark.objects.filter(id=self.bookmark1.id)), 0) + self.assertEqual(len(Bookmark.objects.filter(id=bookmark.id)), 0) def test_archive(self): self.authenticate() + bookmark = self.setup_bookmark() - url = reverse('bookmarks:bookmark-archive', args=[self.bookmark1.id]) + url = reverse('bookmarks:bookmark-archive', args=[bookmark.id]) self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT) - bookmark = Bookmark.objects.get(id=self.bookmark1.id) + bookmark = Bookmark.objects.get(id=bookmark.id) self.assertTrue(bookmark.is_archived) def test_unarchive(self): self.authenticate() + bookmark = self.setup_bookmark(is_archived=True) - url = reverse('bookmarks:bookmark-unarchive', args=[self.archived_bookmark1.id]) + url = reverse('bookmarks:bookmark-unarchive', args=[bookmark.id]) self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT) - bookmark = Bookmark.objects.get(id=self.archived_bookmark1.id) + bookmark = Bookmark.objects.get(id=bookmark.id) self.assertFalse(bookmark.is_archived) def test_check_returns_no_bookmark_if_url_is_not_bookmarked(self): @@ -509,6 +556,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): def test_can_only_access_own_bookmarks(self): self.authenticate() + self.setup_bookmark() + self.setup_bookmark(is_archived=True) other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') inaccessible_bookmark = self.setup_bookmark(user=other_user) @@ -517,11 +566,11 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): url = reverse('bookmarks:bookmark-list') response = self.get(url, expected_status_code=status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) + self.assertEqual(len(response.data['results']), 1) url = reverse('bookmarks:bookmark-archived') response = self.get(url, expected_status_code=status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 2) + self.assertEqual(len(response.data['results']), 1) url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id]) self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND) diff --git a/bookmarks/tests/test_bookmarks_list_template.py b/bookmarks/tests/test_bookmarks_list_template.py index cb65ea5..af5d9d5 100644 --- a/bookmarks/tests/test_bookmarks_list_template.py +++ b/bookmarks/tests/test_bookmarks_list_template.py @@ -55,7 +55,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin): # Edit link edit_url = reverse('bookmarks:edit', args=[bookmark.id]) self.assertInHTML(f''' - Edit + Edit ''', html, count=count) # Archive link self.assertInHTML(f''' diff --git a/bookmarks/tests/test_pagination_tag.py b/bookmarks/tests/test_pagination_tag.py index c63b1a4..31c4cda 100644 --- a/bookmarks/tests/test_pagination_tag.py +++ b/bookmarks/tests/test_pagination_tag.py @@ -113,9 +113,9 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin): self.assertPageLink(rendered_template, page_number, page_number == current_page, expected_occurrences) self.assertTruncationIndicators(rendered_template, 1) - def test_extend_existing_query(self): - rendered_template = self.render_template(100, 10, 2, url='/test?q=cake') - self.assertPrevLink(rendered_template, 1, href='?q=cake&page=1') - self.assertPageLink(rendered_template, 1, False, href='?q=cake&page=1') - self.assertPageLink(rendered_template, 2, True, href='?q=cake&page=2') - self.assertNextLink(rendered_template, 3, href='?q=cake&page=3') + 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') diff --git a/bookmarks/tests/test_queries.py b/bookmarks/tests/test_queries.py index 04e5cb3..4a403fd 100644 --- a/bookmarks/tests/test_queries.py +++ b/bookmarks/tests/test_queries.py @@ -3,9 +3,10 @@ import operator from django.contrib.auth import get_user_model from django.db.models import QuerySet from django.test import TestCase +from django.utils import timezone from bookmarks import queries -from bookmarks.models import Bookmark, UserProfile +from bookmarks.models import Bookmark, BookmarkSearch, UserProfile from bookmarks.tests.helpers import BookmarkFactoryMixin, random_sentence from bookmarks.utils import unique @@ -163,7 +164,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmarks_should_return_all_for_empty_query(self): self.setup_bookmark_search_data() - query = queries.query_bookmarks(self.user, self.profile, '') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [ self.other_bookmarks, self.term1_bookmarks, @@ -178,7 +179,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmarks_should_search_single_term(self): self.setup_bookmark_search_data() - query = queries.query_bookmarks(self.user, self.profile, 'term1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1')) self.assertQueryResult(query, [ self.term1_bookmarks, self.term1_term2_bookmarks, @@ -188,35 +189,35 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmarks_should_search_multiple_terms(self): self.setup_bookmark_search_data() - query = queries.query_bookmarks(self.user, self.profile, 'term2 term1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term2 term1')) self.assertQueryResult(query, [self.term1_term2_bookmarks]) def test_query_bookmarks_should_search_single_tag(self): self.setup_bookmark_search_data() - query = queries.query_bookmarks(self.user, self.profile, '#tag1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1')) self.assertQueryResult(query, [self.tag1_bookmarks, self.tag1_tag2_bookmarks, self.term1_tag1_bookmarks]) def test_query_bookmarks_should_search_multiple_tags(self): self.setup_bookmark_search_data() - query = queries.query_bookmarks(self.user, self.profile, '#tag1 #tag2') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1 #tag2')) self.assertQueryResult(query, [self.tag1_tag2_bookmarks]) def test_query_bookmarks_should_search_multiple_tags_ignoring_casing(self): self.setup_bookmark_search_data() - query = queries.query_bookmarks(self.user, self.profile, '#Tag1 #TAG2') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#Tag1 #TAG2')) self.assertQueryResult(query, [self.tag1_tag2_bookmarks]) def test_query_bookmarks_should_search_terms_and_tags_combined(self): self.setup_bookmark_search_data() - query = queries.query_bookmarks(self.user, self.profile, 'term1 #tag1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #tag1')) self.assertQueryResult(query, [self.term1_tag1_bookmarks]) @@ -226,7 +227,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.profile.tag_search = UserProfile.TAG_SEARCH_STRICT self.profile.save() - query = queries.query_bookmarks(self.user, self.profile, 'tag1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1')) self.assertQueryResult(query, [self.tag1_as_term_bookmarks]) def test_query_bookmarks_in_lax_mode_should_search_tags_as_terms(self): @@ -235,7 +236,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.profile.tag_search = UserProfile.TAG_SEARCH_LAX self.profile.save() - query = queries.query_bookmarks(self.user, self.profile, 'tag1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1')) self.assertQueryResult(query, [ self.tag1_bookmarks, self.tag1_as_term_bookmarks, @@ -243,17 +244,17 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.term1_tag1_bookmarks ]) - query = queries.query_bookmarks(self.user, self.profile, 'tag1 term1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 term1')) self.assertQueryResult(query, [ self.term1_tag1_bookmarks, ]) - query = queries.query_bookmarks(self.user, self.profile, 'tag1 tag2') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 tag2')) self.assertQueryResult(query, [ self.tag1_tag2_bookmarks, ]) - query = queries.query_bookmarks(self.user, self.profile, 'tag1 #tag2') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 #tag2')) self.assertQueryResult(query, [ self.tag1_tag2_bookmarks, ]) @@ -261,28 +262,28 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmarks_should_return_no_matches(self): self.setup_bookmark_search_data() - query = queries.query_bookmarks(self.user, self.profile, 'term3') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term3')) self.assertQueryResult(query, []) - query = queries.query_bookmarks(self.user, self.profile, 'term1 term3') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 term3')) self.assertQueryResult(query, []) - query = queries.query_bookmarks(self.user, self.profile, 'term1 #tag2') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #tag2')) self.assertQueryResult(query, []) - query = queries.query_bookmarks(self.user, self.profile, '#tag3') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag3')) self.assertQueryResult(query, []) # Unused tag - query = queries.query_bookmarks(self.user, self.profile, '#unused_tag1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#unused_tag1')) self.assertQueryResult(query, []) # Unused tag combined with tag that is used - query = queries.query_bookmarks(self.user, self.profile, '#tag1 #unused_tag1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1 #unused_tag1')) self.assertQueryResult(query, []) # Unused tag combined with term that is used - query = queries.query_bookmarks(self.user, self.profile, 'term1 #unused_tag1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #unused_tag1')) self.assertQueryResult(query, []) def test_query_bookmarks_should_not_return_archived_bookmarks(self): @@ -292,7 +293,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True) self.setup_bookmark(is_archived=True) - query = queries.query_bookmarks(self.user, self.profile, '') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [[bookmark1, bookmark2]]) @@ -303,7 +304,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark() self.setup_bookmark() - query = queries.query_archived_bookmarks(self.user, self.profile, '') + query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [[bookmark1, bookmark2]]) @@ -318,7 +319,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(user=other_user) self.setup_bookmark(user=other_user) - query = queries.query_bookmarks(self.user, self.profile, '') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [owned_bookmarks]) @@ -333,7 +334,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True, user=other_user) self.setup_bookmark(is_archived=True, user=other_user) - query = queries.query_archived_bookmarks(self.user, self.profile, '') + query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [owned_bookmarks]) @@ -343,7 +344,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(tags=[tag]) self.setup_bookmark(tags=[tag]) - query = queries.query_bookmarks(self.user, self.profile, '!untagged') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged')) self.assertCountEqual(list(query), [untagged_bookmark]) def test_query_bookmarks_untagged_should_be_combinable_with_search_terms(self): @@ -352,7 +353,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(title='term2') self.setup_bookmark(tags=[tag]) - query = queries.query_bookmarks(self.user, self.profile, '!untagged term1') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged term1')) self.assertCountEqual(list(query), [untagged_bookmark]) def test_query_bookmarks_untagged_should_not_be_combinable_with_tags(self): @@ -361,7 +362,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(tags=[tag]) self.setup_bookmark(tags=[tag]) - query = queries.query_bookmarks(self.user, self.profile, f'!untagged #{tag.name}') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=f'!untagged #{tag.name}')) self.assertCountEqual(list(query), []) def test_query_archived_bookmarks_untagged_should_return_untagged_bookmarks_only(self): @@ -370,7 +371,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True, tags=[tag]) self.setup_bookmark(is_archived=True, tags=[tag]) - query = queries.query_archived_bookmarks(self.user, self.profile, '!untagged') + query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged')) self.assertCountEqual(list(query), [untagged_bookmark]) def test_query_archived_bookmarks_untagged_should_be_combinable_with_search_terms(self): @@ -379,7 +380,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True, title='term2') self.setup_bookmark(is_archived=True, tags=[tag]) - query = queries.query_archived_bookmarks(self.user, self.profile, '!untagged term1') + query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged term1')) self.assertCountEqual(list(query), [untagged_bookmark]) def test_query_archived_bookmarks_untagged_should_not_be_combinable_with_tags(self): @@ -388,7 +389,8 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True, tags=[tag]) self.setup_bookmark(is_archived=True, tags=[tag]) - query = queries.query_archived_bookmarks(self.user, self.profile, f'!untagged #{tag.name}') + query = queries.query_archived_bookmarks(self.user, self.profile, + BookmarkSearch(query=f'!untagged #{tag.name}')) self.assertCountEqual(list(query), []) def test_query_bookmarks_unread_should_return_unread_bookmarks_only(self): @@ -401,7 +403,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark() self.setup_bookmark() - query = queries.query_bookmarks(self.user, self.profile, '!unread') + query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread')) self.assertCountEqual(list(query), unread_bookmarks) def test_query_archived_bookmarks_unread_should_return_unread_bookmarks_only(self): @@ -414,13 +416,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True) self.setup_bookmark(is_archived=True) - query = queries.query_archived_bookmarks(self.user, self.profile, '!unread') + query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread')) self.assertCountEqual(list(query), unread_bookmarks) def test_query_bookmark_tags_should_return_all_tags_for_empty_query(self): self.setup_tag_search_data() - query = queries.query_bookmark_tags(self.user, self.profile, '') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.other_bookmarks), @@ -435,7 +437,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmark_tags_should_search_single_term(self): self.setup_tag_search_data() - query = queries.query_bookmark_tags(self.user, self.profile, 'term1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.term1_bookmarks), @@ -446,7 +448,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmark_tags_should_search_multiple_terms(self): self.setup_tag_search_data() - query = queries.query_bookmark_tags(self.user, self.profile, 'term2 term1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term2 term1')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.term1_term2_bookmarks), @@ -455,7 +457,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmark_tags_should_search_single_tag(self): self.setup_tag_search_data() - query = queries.query_bookmark_tags(self.user, self.profile, '#tag1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.tag1_bookmarks), @@ -466,7 +468,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmark_tags_should_search_multiple_tags(self): self.setup_tag_search_data() - query = queries.query_bookmark_tags(self.user, self.profile, '#tag1 #tag2') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1 #tag2')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks), @@ -475,7 +477,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmark_tags_should_search_multiple_tags_ignoring_casing(self): self.setup_tag_search_data() - query = queries.query_bookmark_tags(self.user, self.profile, '#Tag1 #TAG2') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#Tag1 #TAG2')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks), @@ -484,7 +486,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmark_tags_should_search_term_and_tag_combined(self): self.setup_tag_search_data() - query = queries.query_bookmark_tags(self.user, self.profile, 'term1 #tag1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #tag1')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.term1_tag1_bookmarks), @@ -496,7 +498,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.profile.tag_search = UserProfile.TAG_SEARCH_STRICT self.profile.save() - query = queries.query_bookmark_tags(self.user, self.profile, 'tag1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1')) self.assertQueryResult(query, self.get_tags_from_bookmarks(self.tag1_as_term_bookmarks)) def test_query_bookmark_tags_in_lax_mode_should_search_tags_as_terms(self): @@ -505,7 +507,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.profile.tag_search = UserProfile.TAG_SEARCH_LAX self.profile.save() - query = queries.query_bookmark_tags(self.user, self.profile, 'tag1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.tag1_bookmarks), self.get_tags_from_bookmarks(self.tag1_as_term_bookmarks), @@ -513,17 +515,17 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.get_tags_from_bookmarks(self.term1_tag1_bookmarks) ]) - query = queries.query_bookmark_tags(self.user, self.profile, 'tag1 term1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 term1')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.term1_tag1_bookmarks), ]) - query = queries.query_bookmark_tags(self.user, self.profile, 'tag1 tag2') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 tag2')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks), ]) - query = queries.query_bookmark_tags(self.user, self.profile, 'tag1 #tag2') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 #tag2')) self.assertQueryResult(query, [ self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks), ]) @@ -531,28 +533,28 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): def test_query_bookmark_tags_should_return_no_matches(self): self.setup_tag_search_data() - query = queries.query_bookmark_tags(self.user, self.profile, 'term3') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term3')) self.assertQueryResult(query, []) - query = queries.query_bookmark_tags(self.user, self.profile, 'term1 term3') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 term3')) self.assertQueryResult(query, []) - query = queries.query_bookmark_tags(self.user, self.profile, 'term1 #tag2') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #tag2')) self.assertQueryResult(query, []) - query = queries.query_bookmark_tags(self.user, self.profile, '#tag3') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag3')) self.assertQueryResult(query, []) # Unused tag - query = queries.query_bookmark_tags(self.user, self.profile, '#unused_tag1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#unused_tag1')) self.assertQueryResult(query, []) # Unused tag combined with tag that is used - query = queries.query_bookmark_tags(self.user, self.profile, '#tag1 #unused_tag1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1 #unused_tag1')) self.assertQueryResult(query, []) # Unused tag combined with term that is used - query = queries.query_bookmark_tags(self.user, self.profile, 'term1 #unused_tag1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #unused_tag1')) self.assertQueryResult(query, []) def test_query_bookmark_tags_should_return_tags_for_unarchived_bookmarks_only(self): @@ -562,7 +564,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark() self.setup_bookmark(is_archived=True, tags=[tag2]) - query = queries.query_bookmark_tags(self.user, self.profile, '') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [[tag1]]) @@ -572,7 +574,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(tags=[tag]) self.setup_bookmark(tags=[tag]) - query = queries.query_bookmark_tags(self.user, self.profile, '') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [[tag]]) @@ -583,7 +585,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark() self.setup_bookmark(is_archived=True, tags=[tag2]) - query = queries.query_archived_bookmark_tags(self.user, self.profile, '') + query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [[tag2]]) @@ -593,7 +595,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True, tags=[tag]) self.setup_bookmark(is_archived=True, tags=[tag]) - query = queries.query_archived_bookmark_tags(self.user, self.profile, '') + query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [[tag]]) @@ -608,7 +610,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)]) self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)]) - query = queries.query_bookmark_tags(self.user, self.profile, '') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)]) @@ -623,7 +625,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)]) self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)]) - query = queries.query_archived_bookmark_tags(self.user, self.profile, '') + query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='')) self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)]) @@ -634,13 +636,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(title='term1', tags=[tag]) self.setup_bookmark(tags=[tag]) - query = queries.query_bookmark_tags(self.user, self.profile, '!untagged') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged')) self.assertCountEqual(list(query), []) - query = queries.query_bookmark_tags(self.user, self.profile, '!untagged term1') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged term1')) self.assertCountEqual(list(query), []) - query = queries.query_bookmark_tags(self.user, self.profile, f'!untagged #{tag.name}') + query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=f'!untagged #{tag.name}')) self.assertCountEqual(list(query), []) def test_query_archived_bookmark_tags_untagged_should_never_return_any_tags(self): @@ -650,13 +652,14 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(is_archived=True, title='term1', tags=[tag]) self.setup_bookmark(is_archived=True, tags=[tag]) - query = queries.query_archived_bookmark_tags(self.user, self.profile, '!untagged') + query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged')) self.assertCountEqual(list(query), []) - query = queries.query_archived_bookmark_tags(self.user, self.profile, '!untagged term1') + query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged term1')) self.assertCountEqual(list(query), []) - query = queries.query_archived_bookmark_tags(self.user, self.profile, f'!untagged #{tag.name}') + query = queries.query_archived_bookmark_tags(self.user, self.profile, + BookmarkSearch(query=f'!untagged #{tag.name}')) self.assertCountEqual(list(query), []) def test_query_shared_bookmarks(self): @@ -679,14 +682,14 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(user=user4, shared=True, tags=[tag]), # Should return shared bookmarks from all users - query_set = queries.query_shared_bookmarks(None, self.profile, '', False) + query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=''), False) self.assertQueryResult(query_set, [shared_bookmarks]) # Should respect search query - query_set = queries.query_shared_bookmarks(None, self.profile, 'test title', False) + query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query='test title'), False) self.assertQueryResult(query_set, [[shared_bookmarks[0]]]) - query_set = queries.query_shared_bookmarks(None, self.profile, '#' + tag.name, False) + query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=f'#{tag.name}'), False) self.assertQueryResult(query_set, [[shared_bookmarks[2]]]) def test_query_publicly_shared_bookmarks(self): @@ -696,7 +699,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): bookmark1 = self.setup_bookmark(user=user1, shared=True) self.setup_bookmark(user=user2, shared=True) - query_set = queries.query_shared_bookmarks(None, self.profile, '', True) + query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=''), True) self.assertQueryResult(query_set, [[bookmark1]]) def test_query_shared_bookmark_tags(self): @@ -720,7 +723,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(user=user3, shared=False, tags=[self.setup_tag(user=user3)]), self.setup_bookmark(user=user4, shared=True, tags=[self.setup_tag(user=user4)]), - query_set = queries.query_shared_bookmark_tags(None, self.profile, '', False) + query_set = queries.query_shared_bookmark_tags(None, self.profile, BookmarkSearch(query=''), False) self.assertQueryResult(query_set, [shared_tags]) @@ -734,7 +737,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(user=user1, shared=True, tags=[tag1]), self.setup_bookmark(user=user2, shared=True, tags=[tag2]), - query_set = queries.query_shared_bookmark_tags(None, self.profile, '', True) + query_set = queries.query_shared_bookmark_tags(None, self.profile, BookmarkSearch(query=''), True) self.assertQueryResult(query_set, [[tag1]]) @@ -759,11 +762,11 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(user=users_without_shared_bookmarks[2], shared=True), # Should return users with shared bookmarks - query_set = queries.query_shared_bookmark_users(self.profile, '', False) + query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query=''), False) self.assertQueryResult(query_set, [users_with_shared_bookmarks]) # Should respect search query - query_set = queries.query_shared_bookmark_users(self.profile, 'test title', False) + query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query='test title'), False) self.assertQueryResult(query_set, [[users_with_shared_bookmarks[0]]]) def test_query_publicly_shared_bookmark_users(self): @@ -773,5 +776,91 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin): self.setup_bookmark(user=user1, shared=True) self.setup_bookmark(user=user2, shared=True) - query_set = queries.query_shared_bookmark_users(self.profile, '', True) + query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query=''), True) self.assertQueryResult(query_set, [[user1]]) + + def test_sorty_by_date_added_asc(self): + search = BookmarkSearch(sort=BookmarkSearch.SORT_ADDED_ASC) + + bookmarks = [ + self.setup_bookmark(added=timezone.datetime(2020, 1, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2021, 2, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2022, 3, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2023, 4, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2022, 5, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2021, 6, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2020, 7, 1, tzinfo=timezone.utc)), + ] + sorted_bookmarks = sorted(bookmarks, key=lambda b: b.date_added) + + query = queries.query_bookmarks(self.user, self.profile, search) + self.assertEqual(list(query), sorted_bookmarks) + + def test_sorty_by_date_added_desc(self): + search = BookmarkSearch(sort=BookmarkSearch.SORT_ADDED_DESC) + + bookmarks = [ + self.setup_bookmark(added=timezone.datetime(2020, 1, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2021, 2, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2022, 3, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2023, 4, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2022, 5, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2021, 6, 1, tzinfo=timezone.utc)), + self.setup_bookmark(added=timezone.datetime(2020, 7, 1, tzinfo=timezone.utc)), + ] + sorted_bookmarks = sorted(bookmarks, key=lambda b: b.date_added, reverse=True) + + query = queries.query_bookmarks(self.user, self.profile, search) + self.assertEqual(list(query), sorted_bookmarks) + + def setup_title_sort_data(self): + # lots of combinations to test effective title logic + bookmarks = [ + self.setup_bookmark(title='a_1_1'), + self.setup_bookmark(title='A_1_2'), + self.setup_bookmark(title='b_1_1'), + self.setup_bookmark(title='B_1_2'), + self.setup_bookmark(title='', website_title='a_2_1'), + self.setup_bookmark(title='', website_title='A_2_2'), + self.setup_bookmark(title='', website_title='b_2_1'), + self.setup_bookmark(title='', website_title='B_2_2'), + self.setup_bookmark(title='', website_title='', url='a_3_1'), + self.setup_bookmark(title='', website_title='', url='A_3_2'), + self.setup_bookmark(title='', website_title='', url='b_3_1'), + self.setup_bookmark(title='', website_title='', url='B_3_2'), + self.setup_bookmark(title='a_4_1', website_title='0'), + self.setup_bookmark(title='A_4_2', website_title='0'), + self.setup_bookmark(title='b_4_1', website_title='0'), + self.setup_bookmark(title='B_4_2', website_title='0'), + self.setup_bookmark(title='a_5_1', url='0'), + self.setup_bookmark(title='A_5_2', url='0'), + self.setup_bookmark(title='b_5_1', url='0'), + self.setup_bookmark(title='B_5_2', url='0'), + self.setup_bookmark(title='', website_title='a_6_1', url='0'), + self.setup_bookmark(title='', website_title='A_6_2', url='0'), + self.setup_bookmark(title='', website_title='b_6_1', url='0'), + self.setup_bookmark(title='', website_title='B_6_2', url='0'), + self.setup_bookmark(title='a_7_1', website_title='0', url='0'), + self.setup_bookmark(title='A_7_2', website_title='0', url='0'), + self.setup_bookmark(title='b_7_1', website_title='0', url='0'), + self.setup_bookmark(title='B_7_2', website_title='0', url='0'), + ] + return bookmarks + + def test_sort_by_title_asc(self): + search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_ASC) + + bookmarks = self.setup_title_sort_data() + sorted_bookmarks = sorted(bookmarks, key=lambda b: b.resolved_title.lower()) + + query = queries.query_bookmarks(self.user, self.profile, search) + self.assertEqual(list(query), sorted_bookmarks) + + def test_sort_by_title_desc(self): + search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC) + + bookmarks = self.setup_title_sort_data() + sorted_bookmarks = sorted(bookmarks, key=lambda b: b.resolved_title.lower(), reverse=True) + + query = queries.query_bookmarks(self.user, self.profile, search) + self.assertEqual(list(query), sorted_bookmarks) diff --git a/bookmarks/tests/test_tag_cloud_template.py b/bookmarks/tests/test_tag_cloud_template.py index 8da5a01..ed67ca9 100644 --- a/bookmarks/tests/test_tag_cloud_template.py +++ b/bookmarks/tests/test_tag_cloud_template.py @@ -101,6 +101,18 @@ class TagCloudTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): ], ]) + def test_tag_url_respects_search_options(self): + tag = self.setup_tag(name='tag1') + self.setup_bookmark(tags=[tag], title='term1') + + rendered_template = self.render_template(url='/test?q=term1&sort=title_asc&page=2') + + self.assertInHTML(''' + + tag1 + + ''', rendered_template) + def test_selected_tags(self): tags = [ self.setup_tag(name='tag1'), @@ -191,7 +203,7 @@ class TagCloudTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): ''', rendered_template, count=1) - def test_selected_tag_url_keeps_other_search_terms(self): + def test_selected_tag_url_keeps_other_query_terms(self): tag = self.setup_tag(name='tag1') self.setup_bookmark(tags=[tag], title='term1', description='term2') @@ -204,6 +216,19 @@ class TagCloudTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): ''', rendered_template) + def test_selected_tag_url_respects_search_options(self): + tag = self.setup_tag(name='tag1') + self.setup_bookmark(tags=[tag], title='term1', description='term2') + + rendered_template = self.render_template(url='/test?q=term1 %23tag1 term2&sort=title_asc&page=2') + + self.assertInHTML(''' + + -tag1 + + ''', rendered_template) + def test_selected_tags_are_excluded_from_groups(self): tags = [ self.setup_tag(name='tag1'), diff --git a/bookmarks/tests/test_user_select_tag.py b/bookmarks/tests/test_user_select_tag.py index 82a46b8..1ef1448 100644 --- a/bookmarks/tests/test_user_select_tag.py +++ b/bookmarks/tests/test_user_select_tag.py @@ -2,7 +2,7 @@ from django.db.models import QuerySet from django.template import Template, RequestContext from django.test import TestCase, RequestFactory -from bookmarks.models import BookmarkFilters, User +from bookmarks.models import BookmarkSearch, User from bookmarks.tests.helpers import BookmarkFactoryMixin @@ -12,32 +12,42 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin): request = rf.get(url) request.user = self.get_or_create_test_user() request.user_profile = self.get_or_create_test_user().profile - filters = BookmarkFilters(request) + search = BookmarkSearch.from_request(request) context = RequestContext(request, { 'request': request, - 'filters': filters, + 'search': search, 'users': users, }) template_to_render = Template( '{% load bookmarks %}' - '{% user_select filters users %}' + '{% user_select search users %}' ) return template_to_render.render(context) def assertUserOption(self, html: str, user: User, selected: bool = False): self.assertInHTML(f''' - ''', html) + def assertHiddenInput(self, html: str, name: str, value: str = None): + needle = f'Everyone + ''', rendered_template) def test_render_user_options(self): @@ -60,19 +70,19 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin): self.assertUserOption(rendered_template, user1, True) - def test_render_hidden_inputs_for_filter_params(self): - # Should render hidden inputs if query param exists - url = '/test?q=foo&user=john' + def test_hidden_inputs(self): + # Without params + url = '/test' rendered_template = self.render_template(url) - self.assertInHTML(''' - - ''', rendered_template) + self.assertNoHiddenInput(rendered_template, 'user') + self.assertNoHiddenInput(rendered_template, 'q') + self.assertNoHiddenInput(rendered_template, 'sort') - # Should not render hidden inputs if query param does not exist - url = '/test?user=john' + # With params + url = '/test?q=foo&user=john&sort=title_asc' rendered_template = self.render_template(url) - self.assertInHTML(''' - - ''', rendered_template, count=0) + self.assertNoHiddenInput(rendered_template, 'user') + self.assertHiddenInput(rendered_template, 'q', 'foo') + self.assertHiddenInput(rendered_template, 'sort', 'title_asc') diff --git a/bookmarks/views/bookmarks.py b/bookmarks/views/bookmarks.py index 89cbaa4..45bb57b 100644 --- a/bookmarks/views/bookmarks.py +++ b/bookmarks/views/bookmarks.py @@ -5,7 +5,7 @@ from django.shortcuts import render from django.urls import reverse from bookmarks import queries -from bookmarks.models import Bookmark, BookmarkForm, BookmarkFilters, build_tag_string +from bookmarks.models import Bookmark, BookmarkForm, BookmarkSearch, build_tag_string 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 @@ -36,11 +36,11 @@ def archived(request): def shared(request): - filters = BookmarkFilters(request) + search = BookmarkSearch.from_request(request) bookmark_list = contexts.SharedBookmarkListContext(request) tag_cloud = contexts.SharedTagCloudContext(request) public_only = not request.user.is_authenticated - users = queries.query_shared_bookmark_users(request.user_profile, filters.query, public_only) + users = queries.query_shared_bookmark_users(request.user_profile, search, public_only) return render(request, 'bookmarks/shared.html', { 'bookmark_list': bookmark_list, 'tag_cloud': tag_cloud, @@ -169,15 +169,15 @@ def mark_as_read(request, bookmark_id: int): @login_required def index_action(request): - filters = BookmarkFilters(request) - query = queries.query_bookmarks(request.user, request.user_profile, filters.query) + search = BookmarkSearch.from_request(request) + query = queries.query_bookmarks(request.user, request.user_profile, search) return action(request, query) @login_required def archived_action(request): - filters = BookmarkFilters(request) - query = queries.query_archived_bookmarks(request.user, request.user_profile, filters.query) + search = BookmarkSearch.from_request(request) + query = queries.query_archived_bookmarks(request.user, request.user_profile, search) return action(request, query) diff --git a/bookmarks/views/partials/contexts.py b/bookmarks/views/partials/contexts.py index 63bc1fb..40d68dd 100644 --- a/bookmarks/views/partials/contexts.py +++ b/bookmarks/views/partials/contexts.py @@ -7,8 +7,8 @@ from django.db import models from django.urls import reverse from bookmarks import queries -from bookmarks.models import Bookmark, BookmarkFilters, User, UserProfile, Tag from bookmarks import utils +from bookmarks.models import Bookmark, BookmarkSearch, BookmarkSearchForm, User, UserProfile, Tag DEFAULT_PAGE_SIZE = 30 @@ -55,7 +55,7 @@ class BookmarkItem: class BookmarkListContext: def __init__(self, request: WSGIRequest) -> None: self.request = request - self.filters = BookmarkFilters(self.request) + self.search = BookmarkSearch.from_request(self.request) user = request.user user_profile = request.user_profile @@ -71,29 +71,37 @@ class BookmarkListContext: self.is_empty = paginator.count == 0 self.bookmarks_page = bookmarks_page self.bookmarks_total = paginator.count - self.return_url = self.generate_return_url(page_number) + self.return_url = self.generate_return_url(self.search, self.get_base_url(), page_number) + self.action_url = self.generate_action_url(self.search, self.get_base_action_url(), self.return_url) self.link_target = user_profile.bookmark_link_target self.date_display = user_profile.bookmark_date_display self.show_url = user_profile.display_url self.show_favicons = user_profile.enable_favicons self.show_notes = user_profile.permanent_notes - def generate_return_url(self, page: int): - base_url = self.get_base_url() - url_query = {} - if self.filters.query: - url_query['q'] = self.filters.query - if self.filters.user: - url_query['user'] = self.filters.user + @staticmethod + def generate_return_url(search: BookmarkSearch, base_url: str, page: int = None): + query_params = search.query_params if page is not None: - url_query['page'] = page - url_params = urllib.parse.urlencode(url_query) - return_url = base_url if url_params == '' else base_url + '?' + url_params - return urllib.parse.quote_plus(return_url) + query_params['page'] = page + query_string = urllib.parse.urlencode(query_params) + + return base_url if query_string == '' else base_url + '?' + query_string + + @staticmethod + def generate_action_url(search: BookmarkSearch, base_action_url: str, return_url: str): + query_params = search.query_params + query_params['return_url'] = return_url + query_string = urllib.parse.urlencode(query_params) + + return base_action_url if query_string == '' else base_action_url + '?' + query_string def get_base_url(self): raise Exception(f'Must be implemented by subclass') + def get_base_action_url(self): + raise Exception(f'Must be implemented by subclass') + def get_bookmark_query_set(self): raise Exception(f'Must be implemented by subclass') @@ -102,32 +110,41 @@ class ActiveBookmarkListContext(BookmarkListContext): def get_base_url(self): return reverse('bookmarks:index') + def get_base_action_url(self): + return reverse('bookmarks:index.action') + def get_bookmark_query_set(self): return queries.query_bookmarks(self.request.user, self.request.user_profile, - self.filters.query) + self.search) class ArchivedBookmarkListContext(BookmarkListContext): def get_base_url(self): return reverse('bookmarks:archived') + def get_base_action_url(self): + return reverse('bookmarks:archived.action') + def get_bookmark_query_set(self): return queries.query_archived_bookmarks(self.request.user, self.request.user_profile, - self.filters.query) + self.search) class SharedBookmarkListContext(BookmarkListContext): def get_base_url(self): return reverse('bookmarks:shared') + def get_base_action_url(self): + return reverse('bookmarks:shared.action') + def get_bookmark_query_set(self): - user = User.objects.filter(username=self.filters.user).first() + user = User.objects.filter(username=self.search.user).first() public_only = not self.request.user.is_authenticated return queries.query_shared_bookmarks(user, self.request.user_profile, - self.filters.query, + self.search, public_only) @@ -159,7 +176,7 @@ class TagGroup: class TagCloudContext: def __init__(self, request: WSGIRequest) -> None: self.request = request - self.filters = BookmarkFilters(self.request) + self.search = BookmarkSearch.from_request(self.request) query_set = self.get_tag_query_set() tags = list(query_set) @@ -179,7 +196,7 @@ class TagCloudContext: raise Exception(f'Must be implemented by subclass') def get_selected_tags(self, tags: List[Tag]): - parsed_query = queries.parse_query_string(self.filters.query) + parsed_query = queries.parse_query_string(self.search.query) tag_names = parsed_query['tag_names'] if self.request.user_profile.tag_search == UserProfile.TAG_SEARCH_LAX: tag_names = tag_names + parsed_query['search_terms'] @@ -192,21 +209,21 @@ class ActiveTagCloudContext(TagCloudContext): def get_tag_query_set(self): return queries.query_bookmark_tags(self.request.user, self.request.user_profile, - self.filters.query) + self.search) class ArchivedTagCloudContext(TagCloudContext): def get_tag_query_set(self): return queries.query_archived_bookmark_tags(self.request.user, self.request.user_profile, - self.filters.query) + self.search) class SharedTagCloudContext(TagCloudContext): def get_tag_query_set(self): - user = User.objects.filter(username=self.filters.user).first() + user = User.objects.filter(username=self.search.user).first() public_only = not self.request.user.is_authenticated return queries.query_shared_bookmark_tags(user, self.request.user_profile, - self.filters.query, + self.search, public_only) diff --git a/bookmarks/views/settings.py b/bookmarks/views/settings.py index 3ced6bc..d174805 100644 --- a/bookmarks/views/settings.py +++ b/bookmarks/views/settings.py @@ -12,7 +12,7 @@ from django.shortcuts import render from django.urls import reverse from rest_framework.authtoken.models import Token -from bookmarks.models import UserProfileForm, FeedToken +from bookmarks.models import BookmarkSearch, UserProfileForm, FeedToken from bookmarks.queries import query_bookmarks from bookmarks.services import exporter, tasks from bookmarks.services import importer @@ -136,7 +136,7 @@ def bookmark_import(request): def bookmark_export(request): # noinspection PyBroadException try: - bookmarks = list(query_bookmarks(request.user, request.user_profile, '')) + bookmarks = list(query_bookmarks(request.user, request.user_profile, BookmarkSearch())) # Prefetch tags to prevent n+1 queries prefetch_related_objects(bookmarks, 'tags') file_content = exporter.export_netscape_html(bookmarks) diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py index ef4402d..e54bc6d 100644 --- a/siteroot/settings/base.py +++ b/siteroot/settings/base.py @@ -231,6 +231,10 @@ DATABASES = { 'default': default_database } +SQLITE_ICU_EXTENSION_PATH = './libicu.so' +USE_SQLITE = default_database['ENGINE'] == 'django.db.backends.sqlite3' +USE_SQLITE_ICU_EXTENSION = USE_SQLITE and os.path.exists(SQLITE_ICU_EXTENSION_PATH) + # Favicons LD_DEFAULT_FAVICON_PROVIDER = 'https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url={url}&size=32' LD_FAVICON_PROVIDER = os.getenv('LD_FAVICON_PROVIDER', LD_DEFAULT_FAVICON_PROVIDER)