mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-07 10:58:25 +02:00
Add sort option to bookmark list (#522)
* Rename BookmarkFilters to BookmarkSearch * Refactor queries to accept BookmarkSearch * Sort query by data added and title * Ensure pagination respects search parameters * Ensure tag cloud respects search parameters * Ensure user select respects search parameters * Ensure return url respects search options * Fix passing search options to user select * Fix BookmarkSearch initialization * Extract common search form logic * Ensure partial update respects search options * Add sort UI * Use custom ICU collation when sorting with SQLite * Support sort in API
This commit is contained in:
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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='-')
|
||||
|
@@ -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):
|
||||
|
@@ -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)}`);
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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']
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
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)
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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 */
|
||||
|
@@ -15,13 +15,14 @@
|
||||
<div class="content-area-header mb-0">
|
||||
<h2>Archived bookmarks</h2>
|
||||
<div class="d-flex">
|
||||
{% 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' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions" action="{% url 'bookmarks:archived.action' %}?q={{ bookmark_list.filters.query }}&return_url={{ bookmark_list.return_url }}"
|
||||
method="post">
|
||||
<form class="bookmark-actions"
|
||||
action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post" autocomplete="off">
|
||||
{% csrf_token %}
|
||||
{% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_archive' %}
|
||||
|
||||
|
@@ -65,7 +65,7 @@
|
||||
{% endif %}
|
||||
{% if bookmark_item.is_editable %}
|
||||
{# Bookmark owner actions #}
|
||||
<a href="{% url 'bookmarks:edit' bookmark_item.id %}?return_url={{ bookmark_list.return_url }}">Edit</a>
|
||||
<a href="{% url 'bookmarks:edit' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}">Edit</a>
|
||||
{% if bookmark_item.is_archived %}
|
||||
<button type="submit" name="unarchive" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Unarchive
|
||||
|
@@ -15,12 +15,13 @@
|
||||
<div class="content-area-header mb-0">
|
||||
<h2>Bookmarks</h2>
|
||||
<div class="d-flex">
|
||||
{% bookmark_search bookmark_list.filters tag_cloud.tags %}
|
||||
{% bookmark_search bookmark_list.search tag_cloud.tags %}
|
||||
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions" action="{% url 'bookmarks:index.action' %}?q={{ bookmark_list.filters.query }}&return_url={{ bookmark_list.return_url }}"
|
||||
<form class="bookmark-actions"
|
||||
action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post" autocomplete="off">
|
||||
{% csrf_token %}
|
||||
{% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_unarchive' %}
|
||||
|
@@ -1,15 +1,46 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<div class="search">
|
||||
<form action="" method="get" role="search">
|
||||
<div class="d-flex">
|
||||
<div class="input-group">
|
||||
<span id="search-input-wrap">
|
||||
<input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
|
||||
value="{{ filters.query }}">
|
||||
value="{{ search.query }}">
|
||||
</span>
|
||||
<input type="submit" value="Search" class="btn input-group-btn">
|
||||
</div>
|
||||
{% if filters.user %}
|
||||
<input type="hidden" name="user" value="{{ filters.user }}">
|
||||
{% endif %}
|
||||
<div class="search-options dropdown dropdown-right">
|
||||
<button type="button" class="btn dropdown-toggle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
||||
<path d="M6 4v4"></path>
|
||||
<path d="M6 12v8"></path>
|
||||
<path d="M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
||||
<path d="M12 4v10"></path>
|
||||
<path d="M12 18v2"></path>
|
||||
<path d="M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
||||
<path d="M18 4v1"></path>
|
||||
<path d="M18 9v11"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="menu text-sm" tabindex="0">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.sort.id_for_label }}" class="form-label">Sort by</label>
|
||||
{{ form.sort|add_class:"form-select select-sm" }}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for hidden_field in form.hidden_fields %}
|
||||
{{ hidden_field }}
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
|
@@ -13,10 +13,10 @@
|
||||
<section class="content-area col-2">
|
||||
<div class="content-area-header">
|
||||
<h2>Shared bookmarks</h2>
|
||||
{% bookmark_search bookmark_list.filters tag_cloud.tags mode='shared' %}
|
||||
{% bookmark_search bookmark_list.search tag_cloud.tags mode='shared' %}
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions" action="{% url 'bookmarks:shared.action' %}?return_url={{ bookmark_list.return_url }}"
|
||||
<form class="bookmark-actions" action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<h2>User</h2>
|
||||
</div>
|
||||
<div>
|
||||
{% user_select filters users %}
|
||||
{% user_select bookmark_list.search users %}
|
||||
<br>
|
||||
</div>
|
||||
<div class="content-area-header">
|
||||
|
@@ -1,19 +1,12 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<form id="user-select" action="" method="get">
|
||||
{% if filters.query %}
|
||||
<input type="hidden" name="q" value="{{ filters.query }}">
|
||||
{% endif %}
|
||||
{% for hidden_field in form.hidden_fields %}
|
||||
{{ hidden_field }}
|
||||
{% endfor %}
|
||||
<div class="form-group">
|
||||
<div class="d-flex">
|
||||
<select name="user" class="form-select">
|
||||
<option value="">Everyone</option>
|
||||
{% for user in users %}
|
||||
<option value="{{ user.username }}"
|
||||
{% if user.username == filters.user %}selected{% endif %}
|
||||
data-is-user-option>
|
||||
{{ user.username }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{{ form.user|add_class:"form-select" }}
|
||||
<noscript>
|
||||
<button type="submit" class="btn btn-link ml-2">Apply</button>
|
||||
</noscript>
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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):
|
||||
|
@@ -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'''
|
||||
<a href="{url}">Edit</a>
|
||||
''', html)
|
||||
|
||||
def assertBulkActionForm(self, response, url: str):
|
||||
html = collapse_whitespace(response.content.decode())
|
||||
needle = collapse_whitespace(f'''
|
||||
<form class="bookmark-actions"
|
||||
action="{url}"
|
||||
method="post" autocomplete="off">
|
||||
''')
|
||||
self.assertIn(needle, html)
|
||||
|
||||
def test_should_list_archived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
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)
|
||||
|
@@ -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'''
|
||||
<a href="{url}">Edit</a>
|
||||
''', html)
|
||||
|
||||
def assertBulkActionForm(self, response, url: str):
|
||||
html = collapse_whitespace(response.content.decode())
|
||||
needle = collapse_whitespace(f'''
|
||||
<form class="bookmark-actions"
|
||||
action="{url}"
|
||||
method="post" autocomplete="off">
|
||||
''')
|
||||
self.assertIn(needle, html)
|
||||
|
||||
def test_should_list_unarchived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
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'''
|
||||
<a href="{edit_url}?return_url={return_url}">Edit</a>
|
||||
''', 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'''
|
||||
<a href="{edit_url}?return_url={return_url}">Edit</a>
|
||||
''', 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')
|
||||
|
58
bookmarks/tests/test_bookmark_search_form.py
Normal file
58
bookmarks/tests/test_bookmark_search_form.py
Normal file
@@ -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']])
|
59
bookmarks/tests/test_bookmark_search_model.py
Normal file
59
bookmarks/tests/test_bookmark_search_model.py
Normal file
@@ -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'])
|
@@ -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'<input type="hidden" name="{name}"'
|
||||
if value is not None:
|
||||
needle += f' value="{value}"'
|
||||
|
||||
self.assertIn(needle, html)
|
||||
|
||||
def assertNoHiddenInput(self, html: str, name: str):
|
||||
needle = f'<input type="hidden" name="{name}"'
|
||||
|
||||
self.assertNotIn(needle, html)
|
||||
|
||||
def test_hidden_inputs(self):
|
||||
# Without params
|
||||
url = '/test'
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertInHTML('''
|
||||
<input type="hidden" name="user" value="john">
|
||||
''', 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('''
|
||||
<input type="hidden" name="user" value="john">
|
||||
''', rendered_template, count=0)
|
||||
self.assertHiddenInput(rendered_template, 'user', 'john')
|
||||
self.assertNoHiddenInput(rendered_template, 'q')
|
||||
self.assertNoHiddenInput(rendered_template, 'sort')
|
||||
|
@@ -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 = [
|
||||
'<option value="" selected="">Everyone</option>'
|
||||
]
|
||||
for user in users:
|
||||
self.assertInHTML(f'''
|
||||
<option value="{user.username}" data-is-user-option>
|
||||
{user.username}
|
||||
</option>
|
||||
''', html)
|
||||
user_options.append(f'<option value="{user.username}">{user.username}</option>')
|
||||
user_select_html = f'''
|
||||
<select name="user" class="form-select" required="" id="id_user">
|
||||
{''.join(user_options)}
|
||||
</select>
|
||||
'''
|
||||
|
||||
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'''
|
||||
<option value="{user.username}" data-is-user-option>
|
||||
{user.username}
|
||||
</option>
|
||||
''', html, count=0)
|
||||
<a href="{url}">Edit</a>
|
||||
''', 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)
|
||||
|
@@ -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)
|
||||
|
@@ -55,7 +55,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin):
|
||||
# Edit link
|
||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||
self.assertInHTML(f'''
|
||||
<a href="{edit_url}?return_url=%2Fbookmarks">Edit</a>
|
||||
<a href="{edit_url}?return_url=/bookmarks">Edit</a>
|
||||
''', html, count=count)
|
||||
# Archive link
|
||||
self.assertInHTML(f'''
|
||||
|
@@ -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')
|
||||
|
@@ -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)
|
||||
|
@@ -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('''
|
||||
<a href="?q=term1+%23tag1&sort=title_asc&page=2" class="mr-2" data-is-tag-item>
|
||||
<span class="highlight-char">t</span><span>ag1</span>
|
||||
</a>
|
||||
''', rendered_template)
|
||||
|
||||
def test_selected_tags(self):
|
||||
tags = [
|
||||
self.setup_tag(name='tag1'),
|
||||
@@ -191,7 +203,7 @@ class TagCloudTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
</a>
|
||||
''', 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):
|
||||
</a>
|
||||
''', 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('''
|
||||
<a href="?q=term1+term2&sort=title_asc&page=2"
|
||||
class="text-bold mr-2">
|
||||
<span>-tag1</span>
|
||||
</a>
|
||||
''', rendered_template)
|
||||
|
||||
def test_selected_tags_are_excluded_from_groups(self):
|
||||
tags = [
|
||||
self.setup_tag(name='tag1'),
|
||||
|
@@ -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'''
|
||||
<option value="{user.username}"
|
||||
{'selected' if selected else ''}
|
||||
data-is-user-option>
|
||||
<option value="{user.username}" {'selected' if selected else ''}>
|
||||
{user.username}
|
||||
</option>
|
||||
''', html)
|
||||
|
||||
def assertHiddenInput(self, html: str, name: str, value: str = None):
|
||||
needle = f'<input type="hidden" name="{name}"'
|
||||
if value is not None:
|
||||
needle += f' value="{value}"'
|
||||
|
||||
self.assertIn(needle, html)
|
||||
|
||||
def assertNoHiddenInput(self, html: str, name: str):
|
||||
needle = f'<input type="hidden" name="{name}"'
|
||||
|
||||
self.assertNotIn(needle, html)
|
||||
|
||||
def test_empty_option(self):
|
||||
rendered_template = self.render_template('/test')
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<option value="">Everyone</option>
|
||||
<option value="" selected="">Everyone</option>
|
||||
''', 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('''
|
||||
<input type="hidden" name="q" value="foo">
|
||||
''', 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('''
|
||||
<input type="hidden" name="q" value="foo">
|
||||
''', rendered_template, count=0)
|
||||
self.assertNoHiddenInput(rendered_template, 'user')
|
||||
self.assertHiddenInput(rendered_template, 'q', 'foo')
|
||||
self.assertHiddenInput(rendered_template, 'sort', 'title_asc')
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user