mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-14 22:19:32 +02:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fd3070c6f3 | ||
![]() |
bc374e90a2 | ||
![]() |
a94eb5f85a | ||
![]() |
d1819c6503 | ||
![]() |
353ba433f0 | ||
![]() |
3af4e07eb6 | ||
![]() |
e9061f373a | ||
![]() |
f87398742a | ||
![]() |
81dc19958c | ||
![]() |
5049ff14cf | ||
![]() |
f9ab3d1f44 | ||
![]() |
b89e150088 | ||
![]() |
d17801ba84 | ||
![]() |
7b52663383 | ||
![]() |
0c86587b5d | ||
![]() |
74134d3896 | ||
![]() |
89a9271c71 | ||
![]() |
794b6d8932 | ||
![]() |
6b4664117b | ||
![]() |
621b497dc6 | ||
![]() |
4bb05f811b |
@@ -45,3 +45,5 @@ LD_DB_HOST=
|
||||
# Port use to connect to the database server
|
||||
# Should use the default port if not set
|
||||
LD_DB_PORT=
|
||||
# Any additional options to pass to the database (default: {})
|
||||
LD_DB_OPTIONS=
|
||||
|
41
.github/workflows/main.yaml
vendored
41
.github/workflows/main.yaml
vendored
@@ -3,22 +3,45 @@ name: linkding CI
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
run_tests:
|
||||
name: Run Django Tests
|
||||
unit_tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install Python dependencies
|
||||
run: pip install -r requirements.txt
|
||||
node-version: 18
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
- name: Setup Python environment
|
||||
run: pip install -r requirements.txt
|
||||
- name: Run tests
|
||||
run: python manage.py test
|
||||
run: python manage.py test bookmarks.tests
|
||||
e2e_tests:
|
||||
name: E2E Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
- name: Setup Python environment
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
playwright install chromium
|
||||
python manage.py compilescss
|
||||
python manage.py collectstatic --ignore=*.scss
|
||||
- name: Run tests
|
||||
run: python manage.py test bookmarks.e2e
|
||||
|
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,5 +1,44 @@
|
||||
# Changelog
|
||||
|
||||
## v1.17.2 (18/02/2023)
|
||||
|
||||
### What's Changed
|
||||
* Escape texts in exported HTML by @sissbruecker in https://github.com/sissbruecker/linkding/pull/429
|
||||
* Bump django from 4.1.2 to 4.1.7 by @dependabot in https://github.com/sissbruecker/linkding/pull/427
|
||||
* Make health check in Dockerfile honor context path setting by @mrex in https://github.com/sissbruecker/linkding/pull/407
|
||||
* Disable autocapitalization for tag input form by @joshdick in https://github.com/sissbruecker/linkding/pull/395
|
||||
|
||||
### New Contributors
|
||||
* @mrex made their first contribution in https://github.com/sissbruecker/linkding/pull/407
|
||||
* @joshdick made their first contribution in https://github.com/sissbruecker/linkding/pull/395
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.17.1...v1.17.2
|
||||
|
||||
---
|
||||
|
||||
## v1.17.1 (22/01/2023)
|
||||
|
||||
### What's Changed
|
||||
* Fix favicon being cleared by web archive snapshot task by @sissbruecker in https://github.com/sissbruecker/linkding/pull/405
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.17.0...v1.17.1
|
||||
|
||||
---
|
||||
|
||||
## v1.17.0 (21/01/2023)
|
||||
|
||||
### What's Changed
|
||||
* Add Health Check endpoint by @mckennajones in https://github.com/sissbruecker/linkding/pull/392
|
||||
* Cache website metadata to avoid duplicate scraping by @sissbruecker in https://github.com/sissbruecker/linkding/pull/401
|
||||
* Prefill form if URL is already bookmarked by @sissbruecker in https://github.com/sissbruecker/linkding/pull/402
|
||||
* Add option for showing bookmark favicons by @sissbruecker in https://github.com/sissbruecker/linkding/pull/390
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.16.1...v1.17.0
|
||||
|
||||
---
|
||||
|
||||
## v1.16.1 (20/01/2023)
|
||||
|
||||
### What's Changed
|
||||
|
@@ -53,6 +53,6 @@ RUN ["chmod", "g+w", "."]
|
||||
RUN ["chmod", "+x", "./bootstrap.sh"]
|
||||
|
||||
HEALTHCHECK --interval=30s --retries=3 --timeout=1s \
|
||||
CMD curl -f http://localhost:${LD_SERVER_PORT:-9090}/health || exit 1
|
||||
CMD curl -f http://localhost:${LD_SERVER_PORT:-9090}/${LD_CONTEXT_PATH}health || exit 1
|
||||
|
||||
CMD ["./bootstrap.sh"]
|
||||
|
@@ -193,6 +193,7 @@ This section lists community projects around using linkding, in alphabetical ord
|
||||
- [aiolinkding](https://github.com/bachya/aiolinkding) A Python3, async library to interact with the linkding REST API. By [bachya](https://github.com/bachya)
|
||||
- [linkding-cli](https://github.com/bachya/linkding-cli) A command-line interface (CLI) to interact with the linkding REST API. Powered by [aiolinkding](https://github.com/bachya/aiolinkding). By [bachya](https://github.com/bachya)
|
||||
- [Open all links bookmarklet](https://gist.github.com/ukcuddlyguy/336dd7339e6d35fc64a75ccfc9323c66) A browser bookmarklet to open all links on the current Linkding page in new tabs. By [ukcuddlyguy](https://github.com/ukcuddlyguy)
|
||||
- [LinkThing](https://apps.apple.com/us/app/linkthing/id1666031776) An iOS client for linkding. By [amoscardino](https://github.com/amoscardino)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
@@ -23,7 +23,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
# 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, query_string)
|
||||
return queries.query_bookmarks(user, user.profile, query_string)
|
||||
|
||||
# For single entity actions use default query set without projections
|
||||
return Bookmark.objects.all().filter(owner=user)
|
||||
@@ -35,7 +35,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
def archived(self, request):
|
||||
user = request.user
|
||||
query_string = request.GET.get('q')
|
||||
query_set = queries.query_archived_bookmarks(user, query_string)
|
||||
query_set = queries.query_archived_bookmarks(user, user.profile, query_string)
|
||||
page = self.paginate_queryset(query_set)
|
||||
serializer = self.get_serializer_class()
|
||||
data = serializer(page, many=True).data
|
||||
@@ -45,7 +45,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
||||
def shared(self, request):
|
||||
filters = BookmarkFilters(request)
|
||||
user = User.objects.filter(username=filters.user).first()
|
||||
query_set = queries.query_shared_bookmarks(user, filters.query)
|
||||
query_set = queries.query_shared_bookmarks(user, request.user.profile, filters.query)
|
||||
page = self.paginate_queryset(query_set)
|
||||
serializer = self.get_serializer_class()
|
||||
data = serializer(page, many=True).data
|
||||
|
@@ -119,7 +119,7 @@
|
||||
<div class="form-autocomplete-input form-input" class:is-focused={isFocus}>
|
||||
<!-- autocomplete real input box -->
|
||||
<input id="{id}" name="{name}" value="{value ||''}" placeholder=" "
|
||||
class="form-input" type="text" autocomplete="off"
|
||||
class="form-input" type="text" autocomplete="off" autocapitalize="off"
|
||||
on:input={handleInput} on:keydown={handleKeyDown}
|
||||
on:focus={handleFocus} on:blur={handleBlur}>
|
||||
</div>
|
||||
|
0
bookmarks/e2e/__init__.py
Normal file
0
bookmarks/e2e/__init__.py
Normal file
21
bookmarks/e2e/helpers.py
Normal file
21
bookmarks/e2e/helpers.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.contrib.staticfiles.testing import LiveServerTestCase
|
||||
from playwright.sync_api import BrowserContext
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
||||
def setUp(self) -> None:
|
||||
self.client.force_login(self.get_or_create_test_user())
|
||||
self.cookie = self.client.cookies['sessionid']
|
||||
|
||||
def setup_browser(self, playwright) -> BrowserContext:
|
||||
browser = playwright.chromium.launch(headless=True)
|
||||
context = browser.new_context()
|
||||
context.add_cookies([{
|
||||
'name': 'sessionid',
|
||||
'value': self.cookie.value,
|
||||
'domain': self.live_server_url.replace('http:', ''),
|
||||
'path': '/'
|
||||
}])
|
||||
return context
|
51
bookmarks/e2e/test_bookmark_form.py
Normal file
51
bookmarks/e2e/test_bookmark_form.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from django.urls import reverse
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||
|
||||
|
||||
class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
||||
def test_create_should_check_for_existing_bookmark(self):
|
||||
existing_bookmark = self.setup_bookmark(title='Existing title',
|
||||
description='Existing description',
|
||||
tags=[self.setup_tag(name='tag1'), self.setup_tag(name='tag2')],
|
||||
website_title='Existing website title',
|
||||
website_description='Existing website description',
|
||||
unread=True)
|
||||
tag_names = ' '.join(existing_bookmark.tag_names)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:new'))
|
||||
|
||||
# Enter bookmarked URL
|
||||
page.get_by_label('URL').fill(existing_bookmark.url)
|
||||
# Already bookmarked hint should be visible
|
||||
page.get_by_text('This URL is already bookmarked.').wait_for(timeout=2000)
|
||||
# Form should be pre-filled with data from existing bookmark
|
||||
self.assertEqual(existing_bookmark.title, page.get_by_label('Title').input_value())
|
||||
self.assertEqual(existing_bookmark.description, page.get_by_label('Description').input_value())
|
||||
self.assertEqual(existing_bookmark.website_title, page.get_by_label('Title').get_attribute('placeholder'))
|
||||
self.assertEqual(existing_bookmark.website_description,
|
||||
page.get_by_label('Description').get_attribute('placeholder'))
|
||||
self.assertEqual(tag_names, page.get_by_label('Tags').input_value())
|
||||
self.assertTrue(tag_names, page.get_by_label('Mark as unread').is_checked())
|
||||
|
||||
# Enter non-bookmarked URL
|
||||
page.get_by_label('URL').fill('https://example.com/unknown')
|
||||
# Already bookmarked hint should be hidden
|
||||
page.get_by_text('This URL is already bookmarked.').wait_for(state='hidden', timeout=2000)
|
||||
|
||||
browser.close()
|
||||
|
||||
def test_edit_should_not_check_for_existing_bookmark(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
|
||||
page.wait_for_timeout(timeout=1000)
|
||||
page.get_by_text('This URL is already bookmarked.').wait_for(state='hidden')
|
30
bookmarks/e2e/test_global_shortcuts.py
Normal file
30
bookmarks/e2e/test_global_shortcuts.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.urls import reverse
|
||||
from playwright.sync_api import sync_playwright, expect
|
||||
|
||||
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||
|
||||
|
||||
class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase):
|
||||
def test_focus_search(self):
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:index'))
|
||||
|
||||
page.press('body', 's')
|
||||
|
||||
expect(page.get_by_placeholder('Search for words or #tags')).to_be_focused()
|
||||
|
||||
browser.close()
|
||||
|
||||
def test_add_bookmark(self):
|
||||
with sync_playwright() as p:
|
||||
browser = self.setup_browser(p)
|
||||
page = browser.new_page()
|
||||
page.goto(self.live_server_url + reverse('bookmarks:index'))
|
||||
|
||||
page.press('body', 'n')
|
||||
|
||||
expect(page).to_have_url(self.live_server_url + reverse('bookmarks:new'))
|
||||
|
||||
browser.close()
|
@@ -18,7 +18,7 @@ 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, query_string)
|
||||
query_set = queries.query_bookmarks(feed_token.user, feed_token.user.profile, query_string)
|
||||
return FeedContext(feed_token, query_set)
|
||||
|
||||
def item_title(self, item: Bookmark):
|
||||
|
18
bookmarks/migrations/0020_userprofile_tag_search.py
Normal file
18
bookmarks/migrations/0020_userprofile_tag_search.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.7 on 2023-04-10 01:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0019_userprofile_enable_favicons'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='tag_search',
|
||||
field=models.CharField(choices=[('strict', 'Strict'), ('lax', 'Lax')], default='strict', max_length=10),
|
||||
),
|
||||
]
|
18
bookmarks/migrations/0021_userprofile_display_url.py
Normal file
18
bookmarks/migrations/0021_userprofile_display_url.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.7 on 2023-05-18 07:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0020_userprofile_tag_search'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='display_url',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
@@ -153,6 +153,12 @@ class UserProfile(models.Model):
|
||||
(WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'),
|
||||
(WEB_ARCHIVE_INTEGRATION_ENABLED, 'Enabled'),
|
||||
]
|
||||
TAG_SEARCH_STRICT = 'strict'
|
||||
TAG_SEARCH_LAX = 'lax'
|
||||
TAG_SEARCH_CHOICES = [
|
||||
(TAG_SEARCH_STRICT, 'Strict'),
|
||||
(TAG_SEARCH_LAX, 'Lax'),
|
||||
]
|
||||
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE)
|
||||
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO)
|
||||
bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, blank=False,
|
||||
@@ -161,14 +167,18 @@ class UserProfile(models.Model):
|
||||
default=BOOKMARK_LINK_TARGET_BLANK)
|
||||
web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False,
|
||||
default=WEB_ARCHIVE_INTEGRATION_DISABLED)
|
||||
tag_search = models.CharField(max_length=10, choices=TAG_SEARCH_CHOICES, blank=False,
|
||||
default=TAG_SEARCH_STRICT)
|
||||
enable_sharing = models.BooleanField(default=False, null=False)
|
||||
enable_favicons = models.BooleanField(default=False, null=False)
|
||||
display_url = models.BooleanField(default=False, null=False)
|
||||
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'enable_sharing', 'enable_favicons']
|
||||
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'tag_search',
|
||||
'enable_sharing', 'enable_favicons', 'display_url']
|
||||
|
||||
|
||||
@receiver(post_save, sender=get_user_model())
|
||||
|
@@ -1,29 +1,29 @@
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.db.models import Q, QuerySet, Exists, OuterRef
|
||||
|
||||
from bookmarks.models import Bookmark, Tag
|
||||
from bookmarks.models import Bookmark, Tag, UserProfile
|
||||
from bookmarks.utils import unique
|
||||
|
||||
|
||||
def query_bookmarks(user: User, query_string: str) -> QuerySet:
|
||||
return _base_bookmarks_query(user, query_string) \
|
||||
def query_bookmarks(user: User, profile: UserProfile, query_string: str) -> QuerySet:
|
||||
return _base_bookmarks_query(user, profile, query_string) \
|
||||
.filter(is_archived=False)
|
||||
|
||||
|
||||
def query_archived_bookmarks(user: User, query_string: str) -> QuerySet:
|
||||
return _base_bookmarks_query(user, query_string) \
|
||||
def query_archived_bookmarks(user: User, profile: UserProfile, query_string: str) -> QuerySet:
|
||||
return _base_bookmarks_query(user, profile, query_string) \
|
||||
.filter(is_archived=True)
|
||||
|
||||
|
||||
def query_shared_bookmarks(user: Optional[User], query_string: str) -> QuerySet:
|
||||
return _base_bookmarks_query(user, query_string) \
|
||||
def query_shared_bookmarks(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet:
|
||||
return _base_bookmarks_query(user, profile, query_string) \
|
||||
.filter(shared=True) \
|
||||
.filter(owner__profile__enable_sharing=True)
|
||||
|
||||
|
||||
def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet:
|
||||
def _base_bookmarks_query(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet:
|
||||
query_set = Bookmark.objects
|
||||
|
||||
# Filter for user
|
||||
@@ -35,13 +35,16 @@ def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet:
|
||||
|
||||
# Filter for search terms and tags
|
||||
for term in query['search_terms']:
|
||||
query_set = query_set.filter(
|
||||
Q(title__contains=term)
|
||||
| Q(description__contains=term)
|
||||
| Q(website_title__contains=term)
|
||||
| Q(website_description__contains=term)
|
||||
| Q(url__contains=term)
|
||||
)
|
||||
conditions = Q(title__icontains=term) \
|
||||
| Q(description__icontains=term) \
|
||||
| Q(website_title__icontains=term) \
|
||||
| Q(website_description__icontains=term) \
|
||||
| Q(url__icontains=term)
|
||||
|
||||
if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
|
||||
conditions = conditions | Exists(Bookmark.objects.filter(id=OuterRef('id'), tags__name__iexact=term))
|
||||
|
||||
query_set = query_set.filter(conditions)
|
||||
|
||||
for tag_name in query['tag_names']:
|
||||
query_set = query_set.filter(
|
||||
@@ -65,32 +68,32 @@ def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet:
|
||||
return query_set
|
||||
|
||||
|
||||
def query_bookmark_tags(user: User, query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_bookmarks(user, query_string)
|
||||
def query_bookmark_tags(user: User, profile: UserProfile, query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_bookmarks(user, profile, query_string)
|
||||
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_archived_bookmark_tags(user: User, query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_archived_bookmarks(user, query_string)
|
||||
def query_archived_bookmark_tags(user: User, profile: UserProfile, query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_archived_bookmarks(user, profile, query_string)
|
||||
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_shared_bookmark_tags(user: Optional[User], query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_shared_bookmarks(user, query_string)
|
||||
def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_shared_bookmarks(user, profile, query_string)
|
||||
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_shared_bookmark_users(query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_shared_bookmarks(None, query_string)
|
||||
def query_shared_bookmark_users(profile: UserProfile, query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_shared_bookmarks(None, profile, query_string)
|
||||
|
||||
query_set = User.objects.filter(bookmark__in=bookmarks_query)
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
from typing import List
|
||||
|
||||
from bookmarks.models import Bookmark
|
||||
@@ -28,8 +29,8 @@ def append_list_start(doc: BookmarkDocument):
|
||||
|
||||
def append_bookmark(doc: BookmarkDocument, bookmark: Bookmark):
|
||||
url = bookmark.url
|
||||
title = bookmark.resolved_title
|
||||
desc = bookmark.resolved_description
|
||||
title = html.escape(bookmark.resolved_title or '')
|
||||
desc = html.escape(bookmark.resolved_description or '')
|
||||
tags = ','.join(bookmark.tag_names)
|
||||
toread = '1' if bookmark.unread else '0'
|
||||
added = int(bookmark.date_added.timestamp())
|
||||
|
@@ -10,8 +10,8 @@ from waybackpy.exceptions import WaybackError, TooManyRequestsError, NoCDXRecord
|
||||
|
||||
import bookmarks.services.wayback
|
||||
from bookmarks.models import Bookmark, UserProfile
|
||||
from bookmarks.services.website_loader import DEFAULT_USER_AGENT
|
||||
from bookmarks.services import favicon_loader
|
||||
from bookmarks.services.website_loader import DEFAULT_USER_AGENT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +37,7 @@ def _load_newest_snapshot(bookmark: Bookmark):
|
||||
|
||||
if existing_snapshot:
|
||||
bookmark.web_archive_snapshot_url = existing_snapshot.archive_url
|
||||
bookmark.save()
|
||||
bookmark.save(update_fields=['web_archive_snapshot_url'])
|
||||
logger.info(f'Using newest snapshot. url={bookmark.url} from={existing_snapshot.datetime_timestamp}')
|
||||
|
||||
except NoCDXRecordFound:
|
||||
@@ -51,7 +51,7 @@ def _create_snapshot(bookmark: Bookmark):
|
||||
archive = waybackpy.WaybackMachineSaveAPI(bookmark.url, DEFAULT_USER_AGENT, max_tries=1)
|
||||
archive.save()
|
||||
bookmark.web_archive_snapshot_url = archive.archive_url
|
||||
bookmark.save()
|
||||
bookmark.save(update_fields=['web_archive_snapshot_url'])
|
||||
logger.info(f'Successfully created new snapshot for bookmark:. url={bookmark.url}')
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ def _load_favicon_task(bookmark_id: int):
|
||||
|
||||
if new_favicon != bookmark.favicon_file:
|
||||
bookmark.favicon_file = new_favicon
|
||||
bookmark.save()
|
||||
bookmark.save(update_fields=['favicon_file'])
|
||||
logger.info(f'Successfully updated favicon for bookmark. url={bookmark.url} icon={new_favicon}')
|
||||
|
||||
|
||||
|
@@ -102,3 +102,12 @@ a:visited:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
// Increase input font size on small viewports to prevent zooming on focus the input
|
||||
// on mobile devices. 430px relates to the "normalized" iPhone 14 Pro Max
|
||||
// viewport size
|
||||
@media screen and (max-width: 430px) {
|
||||
.form-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
@@ -64,6 +64,10 @@ ul.bookmark-list {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.url-display {
|
||||
color: $secondary-link-color;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: $gray-color-dark;
|
||||
|
||||
|
@@ -21,6 +21,8 @@ $link-color: $primary-color !default;
|
||||
$link-color-dark: darken($link-color, 5%) !default;
|
||||
$link-color-light: $link-color !default;
|
||||
|
||||
$secondary-link-color: rgba(168, 177, 255, 0.73);
|
||||
|
||||
$alternative-color: #59bdb9;
|
||||
$alternative-color-dark: #73f1eb;
|
||||
|
||||
|
@@ -2,3 +2,5 @@ $html-font-size: 18px !default;
|
||||
|
||||
$alternative-color: #05a6a3;
|
||||
$alternative-color-dark: darken($alternative-color, 5%);
|
||||
|
||||
$secondary-link-color: rgba(87, 85, 217, 0.64);
|
@@ -18,11 +18,19 @@
|
||||
{{ bookmark.resolved_title }}
|
||||
</a>
|
||||
</div>
|
||||
{% if request.user.profile.display_url %}
|
||||
<div class="url-path truncate">
|
||||
<a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
|
||||
class="url-display text-sm">
|
||||
{{ bookmark.url }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="description truncate">
|
||||
{% if bookmark.tag_names %}
|
||||
<span>
|
||||
{% for tag_name in bookmark.tag_names %}
|
||||
<a href="?{% append_to_query_param q=tag_name|hash_tag %}">{{ tag_name|hash_tag }}</a>
|
||||
<a href="?{% add_tag_to_query tag_name %}">{{ tag_name|hash_tag }}</a>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tag_string.id_for_label }}" class="form-label">Tags</label>
|
||||
{{ form.tag_string|add_class:"form-input"|attr:"autocomplete:off" }}
|
||||
{{ form.tag_string|add_class:"form-input"|attr:"autocomplete:off"|attr:"autocapitalize:off" }}
|
||||
<div class="form-input-hint">
|
||||
Enter any number of tags separated by space and <strong>without</strong> the hash (#). If a tag does not
|
||||
exist it will be
|
||||
|
@@ -4,7 +4,7 @@
|
||||
{% if has_selected_tags %}
|
||||
<p class="selected-tags">
|
||||
{% for tag in selected_tags %}
|
||||
<a href="?{% remove_from_query_param q=tag.name|hash_tag %}"
|
||||
<a href="?{% remove_tag_from_query tag.name %}"
|
||||
class="text-bold mr-2">
|
||||
<span>-{{ tag.name }}</span>
|
||||
</a>
|
||||
@@ -17,14 +17,14 @@
|
||||
{% for tag in group.tags %}
|
||||
{# Highlight first char of first tag in group #}
|
||||
{% if forloop.counter == 1 %}
|
||||
<a href="?{% append_to_query_param q=tag.name|hash_tag %}"
|
||||
<a href="?{% add_tag_to_query tag.name %}"
|
||||
class="mr-2" data-is-tag-item>
|
||||
<span
|
||||
class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{# Render remaining tags normally #}
|
||||
<a href="?{% append_to_query_param q=tag.name|hash_tag %}"
|
||||
<a href="?{% add_tag_to_query tag.name %}"
|
||||
class="mr-2" data-is-tag-item>
|
||||
<span>{{ tag.name }}</span>
|
||||
</a>
|
||||
|
@@ -29,6 +29,15 @@
|
||||
be hidden.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.display_url.id_for_label }}" class="form-checkbox">
|
||||
{{ form.display_url }}
|
||||
<i class="form-icon"></i> Show bookmark URL
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
When enabled, this setting displays the bookmark URL below the title.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.bookmark_link_target.id_for_label }}" class="form-label">Open bookmarks in</label>
|
||||
{{ form.bookmark_link_target|add_class:"form-select col-2 col-sm-12" }}
|
||||
@@ -36,6 +45,15 @@
|
||||
Whether to open bookmarks a new page or in the same page.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tag_search.id_for_label }}" class="form-label">Tag search</label>
|
||||
{{ form.tag_search|add_class:"form-select col-2 col-sm-12" }}
|
||||
<div class="form-input-hint">
|
||||
In strict mode, tags must be prefixed with a hash character (#).
|
||||
In lax mode, tags can also be searched without the hash character.
|
||||
Note that tags without the hash character are indistinguishable from search terms, which means the search result will also include bookmarks where a search term matches otherwise.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_favicons.id_for_label }}" class="form-checkbox">
|
||||
{{ form.enable_favicons }}
|
||||
|
@@ -3,6 +3,7 @@ import re
|
||||
from django import template
|
||||
|
||||
from bookmarks import utils
|
||||
from bookmarks.models import UserProfile
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -19,36 +20,39 @@ def update_query_string(context, **kwargs):
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def append_to_query_param(context, **kwargs):
|
||||
query = context.request.GET.copy()
|
||||
def add_tag_to_query(context, tag_name: str):
|
||||
params = context.request.GET.copy()
|
||||
|
||||
# Append to or create query param
|
||||
for key in kwargs:
|
||||
if query.__contains__(key):
|
||||
value = query.__getitem__(key) + ' '
|
||||
else:
|
||||
value = ''
|
||||
value = value + kwargs[key]
|
||||
query.__setitem__(key, value)
|
||||
# Append to or create query string
|
||||
if params.__contains__('q'):
|
||||
query_string = params.__getitem__('q') + ' '
|
||||
else:
|
||||
query_string = ''
|
||||
query_string = query_string + '#' + tag_name
|
||||
params.__setitem__('q', query_string)
|
||||
|
||||
return query.urlencode()
|
||||
return params.urlencode()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def remove_from_query_param(context, **kwargs):
|
||||
query = context.request.GET.copy()
|
||||
def remove_tag_from_query(context, tag_name: str):
|
||||
params = context.request.GET.copy()
|
||||
if params.__contains__('q'):
|
||||
# Split query string into parts
|
||||
query_string = params.__getitem__('q')
|
||||
query_parts = query_string.split()
|
||||
# Remove tag with hash
|
||||
tag_name_with_hash = '#' + tag_name
|
||||
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name_with_hash)]
|
||||
# When using lax tag search, also remove tag without hash
|
||||
profile = context.request.user.profile
|
||||
if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
|
||||
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name)]
|
||||
# Rebuild query string
|
||||
query_string = ' '.join(query_parts)
|
||||
params.__setitem__('q', query_string)
|
||||
|
||||
# Remove item from query param
|
||||
for key in kwargs:
|
||||
if query.__contains__(key):
|
||||
value = query.__getitem__(key)
|
||||
parts = value.split()
|
||||
part_to_remove = kwargs[key]
|
||||
updated_parts = [part for part in parts if str.lower(part) != str.lower(part_to_remove)]
|
||||
updated_value = ' '.join(updated_parts)
|
||||
query.__setitem__(key, updated_value)
|
||||
|
||||
return query.urlencode()
|
||||
return params.urlencode()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
|
@@ -158,6 +158,37 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||
|
||||
def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(self):
|
||||
tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
self.setup_bookmark(title=tags[0].name, tags=tags, is_archived=True)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived') + f'?q={tags[0].name}+%23{tags[1].name.upper()}')
|
||||
|
||||
self.assertSelectedTags(response, [tags[1]])
|
||||
|
||||
def test_should_display_search_terms_from_query_as_selected_tags_in_lax_mode(self):
|
||||
self.user.profile.tag_search = UserProfile.TAG_SEARCH_LAX
|
||||
self.user.profile.save()
|
||||
|
||||
tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
self.setup_bookmark(tags=tags, is_archived=True)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived') + f'?q={tags[0].name}+%23{tags[1].name.upper()}')
|
||||
|
||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||
|
||||
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(is_archived=True),
|
||||
|
@@ -87,7 +87,7 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag_string = build_tag_string(bookmark.tag_names, ' ')
|
||||
self.assertInHTML(f'''
|
||||
<input type="text" name="tag_string" value="{tag_string}"
|
||||
autocomplete="off" class="form-input" id="id_tag_string">
|
||||
autocomplete="off" autocapitalize="off" class="form-input" id="id_tag_string">
|
||||
''', html)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
|
@@ -155,7 +155,38 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
]
|
||||
self.setup_bookmark(tags=tags)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + f'?q=%23{tags[0].name}+%23{tags[1].name}')
|
||||
response = self.client.get(reverse('bookmarks:index') + f'?q=%23{tags[0].name}+%23{tags[1].name.upper()}')
|
||||
|
||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||
|
||||
def test_should_not_display_search_terms_from_query_as_selected_tags_in_strict_mode(self):
|
||||
tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
self.setup_bookmark(title=tags[0].name, tags=tags)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + f'?q={tags[0].name}+%23{tags[1].name.upper()}')
|
||||
|
||||
self.assertSelectedTags(response, [tags[1]])
|
||||
|
||||
def test_should_display_search_terms_from_query_as_selected_tags_in_lax_mode(self):
|
||||
self.user.profile.tag_search = UserProfile.TAG_SEARCH_LAX
|
||||
self.user.profile.save()
|
||||
|
||||
tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
self.setup_bookmark(tags=tags)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + f'?q={tags[0].name}+%23{tags[1].name.upper()}')
|
||||
|
||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||
|
||||
|
@@ -90,6 +90,25 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||
<img src="/static/{bookmark.favicon_file}" alt="">
|
||||
''', html, count=count)
|
||||
|
||||
def assertBookmarkURLCount(self, html: str, bookmark: Bookmark, link_target: str = '_blank', count=0):
|
||||
self.assertInHTML(f'''
|
||||
<div class="url-path truncate">
|
||||
<a href="{ bookmark.url }" target="{ link_target }" rel="noopener"
|
||||
class="url-display text-sm">
|
||||
{ bookmark.url }
|
||||
</a>
|
||||
</div>
|
||||
''', html, count)
|
||||
|
||||
def assertBookmarkURLVisible(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkURLCount(html, bookmark, count=1)
|
||||
|
||||
|
||||
def assertBookmarkURLHidden(self, html: str, bookmark: Bookmark, link_target: str = '_blank'):
|
||||
self.assertBookmarkURLCount(html, bookmark, count=0)
|
||||
|
||||
|
||||
|
||||
def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test') -> str:
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
@@ -252,3 +271,34 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertFaviconHidden(html, bookmark)
|
||||
|
||||
def test_bookmark_url_should_be_hidden_by_default(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertBookmarkURLHidden(html,bookmark)
|
||||
|
||||
def test_show_bookmark_url_when_enabled(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.display_url = True
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertBookmarkURLVisible(html,bookmark)
|
||||
|
||||
def test_hide_bookmark_url_when_disabled(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.display_url = False
|
||||
profile.save()
|
||||
|
||||
bookmark = self.setup_bookmark()
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertBookmarkURLHidden(html,bookmark)
|
||||
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
import waybackpy
|
||||
@@ -15,15 +16,14 @@ from bookmarks.services import tasks
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging
|
||||
|
||||
|
||||
class MockWaybackMachineSaveAPI:
|
||||
def __init__(self, archive_url: str = 'https://example.com/created_snapshot', fail_on_save: bool = False):
|
||||
self.archive_url = archive_url
|
||||
self.fail_on_save = fail_on_save
|
||||
def create_wayback_machine_save_api_mock(archive_url: str = 'https://example.com/created_snapshot',
|
||||
fail_on_save: bool = False):
|
||||
mock_api = mock.Mock(archive_url=archive_url)
|
||||
|
||||
def save(self):
|
||||
if self.fail_on_save:
|
||||
raise WaybackError
|
||||
return self
|
||||
if fail_on_save:
|
||||
mock_api.save.side_effect = WaybackError
|
||||
|
||||
return mock_api
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -32,21 +32,18 @@ class MockCdxSnapshot:
|
||||
datetime_timestamp: datetime.datetime
|
||||
|
||||
|
||||
class MockWaybackMachineCDXServerAPI:
|
||||
def __init__(self,
|
||||
archive_url: str = 'https://example.com/newest_snapshot',
|
||||
has_no_snapshot=False,
|
||||
fail_loading_snapshot=False):
|
||||
self.archive_url = archive_url
|
||||
self.has_no_snapshot = has_no_snapshot
|
||||
self.fail_loading_snapshot = fail_loading_snapshot
|
||||
def create_cdx_server_api_mock(archive_url: str | None = 'https://example.com/newest_snapshot',
|
||||
fail_loading_snapshot=False):
|
||||
mock_api = mock.Mock()
|
||||
|
||||
def newest(self):
|
||||
if self.has_no_snapshot:
|
||||
return None
|
||||
if self.fail_loading_snapshot:
|
||||
raise WaybackError
|
||||
return MockCdxSnapshot(self.archive_url, datetime.datetime.now())
|
||||
if fail_loading_snapshot:
|
||||
mock_api.newest.side_effect = WaybackError
|
||||
elif archive_url:
|
||||
mock_api.newest.return_value = MockCdxSnapshot(archive_url, datetime.datetime.now())
|
||||
else:
|
||||
mock_api.newest.return_value = None
|
||||
|
||||
return mock_api
|
||||
|
||||
|
||||
class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
@@ -58,7 +55,7 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
user.profile.save()
|
||||
|
||||
@disable_logging
|
||||
def run_pending_task(self, task_function):
|
||||
def run_pending_task(self, task_function: Any):
|
||||
func = getattr(task_function, 'task_function', None)
|
||||
task = Task.objects.all()[0]
|
||||
self.assertEqual(task_function.name, task.task_name)
|
||||
@@ -67,7 +64,7 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
task.delete()
|
||||
|
||||
@disable_logging
|
||||
def run_all_pending_tasks(self, task_function):
|
||||
def run_all_pending_tasks(self, task_function: Any):
|
||||
func = getattr(task_function, 'task_function', None)
|
||||
tasks = Task.objects.all()
|
||||
|
||||
@@ -79,27 +76,30 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_create_web_archive_snapshot_should_update_snapshot_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_save_api = create_wayback_machine_save_api_mock()
|
||||
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=MockWaybackMachineSaveAPI()):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
mock_save_api.save.assert_called_once()
|
||||
self.assertEqual(bookmark.web_archive_snapshot_url, 'https://example.com/created_snapshot')
|
||||
|
||||
def test_create_web_archive_snapshot_should_handle_missing_bookmark_id(self):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI()) as mock_save_api:
|
||||
mock_save_api = create_wayback_machine_save_api_mock()
|
||||
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api):
|
||||
tasks._create_web_archive_snapshot_task(123, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
mock_save_api.assert_not_called()
|
||||
mock_save_api.save.assert_not_called()
|
||||
|
||||
def test_create_web_archive_snapshot_should_skip_if_snapshot_exists(self):
|
||||
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com')
|
||||
mock_save_api = create_wayback_machine_save_api_mock()
|
||||
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI()) as mock_save_api:
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
@@ -107,9 +107,9 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_create_web_archive_snapshot_should_force_update_snapshot(self):
|
||||
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com')
|
||||
mock_save_api = create_wayback_machine_save_api_mock(archive_url='https://other.com')
|
||||
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI('https://other.com')):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, True)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
bookmark.refresh_from_db()
|
||||
@@ -118,24 +118,27 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_create_web_archive_snapshot_should_use_newest_snapshot_as_fallback(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True)
|
||||
mock_cdx_api = create_cdx_server_api_mock()
|
||||
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()):
|
||||
return_value=mock_cdx_api):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
mock_cdx_api.newest.assert_called_once()
|
||||
self.assertEqual('https://example.com/newest_snapshot', bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_create_web_archive_snapshot_should_ignore_missing_newest_snapshot(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True)
|
||||
mock_cdx_api = create_cdx_server_api_mock(archive_url=None)
|
||||
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(has_no_snapshot=True)):
|
||||
return_value=mock_cdx_api):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
@@ -144,51 +147,78 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_create_web_archive_snapshot_should_ignore_newest_snapshot_errors(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_save_api = create_wayback_machine_save_api_mock(fail_on_save=True)
|
||||
mock_cdx_api = create_cdx_server_api_mock(fail_loading_snapshot=True)
|
||||
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI',
|
||||
return_value=MockWaybackMachineSaveAPI(fail_on_save=True)):
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api):
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(fail_loading_snapshot=True)):
|
||||
return_value=mock_cdx_api):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
self.assertEqual('', bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_create_web_archive_snapshot_should_not_save_stale_bookmark_data(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_save_api = create_wayback_machine_save_api_mock()
|
||||
|
||||
# update bookmark during API call to check that saving
|
||||
# the snapshot does not overwrite updated bookmark data
|
||||
def mock_save_impl():
|
||||
bookmark.title = 'Updated title'
|
||||
bookmark.save()
|
||||
|
||||
mock_save_api.save.side_effect = mock_save_impl
|
||||
|
||||
with mock.patch.object(waybackpy, 'WaybackMachineSaveAPI', return_value=mock_save_api):
|
||||
tasks.create_web_archive_snapshot(self.get_or_create_test_user(), bookmark, False)
|
||||
self.run_pending_task(tasks._create_web_archive_snapshot_task)
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertEqual(bookmark.title, 'Updated title')
|
||||
self.assertEqual('https://example.com/created_snapshot', bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_load_web_archive_snapshot_should_update_snapshot_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_cdx_api = create_cdx_server_api_mock()
|
||||
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()):
|
||||
return_value=mock_cdx_api):
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
mock_cdx_api.newest.assert_called_once()
|
||||
self.assertEqual('https://example.com/newest_snapshot', bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_load_web_archive_snapshot_should_handle_missing_bookmark_id(self):
|
||||
mock_cdx_api = create_cdx_server_api_mock()
|
||||
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()) as mock_cdx_api:
|
||||
return_value=mock_cdx_api):
|
||||
tasks._load_web_archive_snapshot_task(123)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
mock_cdx_api.assert_not_called()
|
||||
mock_cdx_api.newest.assert_not_called()
|
||||
|
||||
def test_load_web_archive_snapshot_should_skip_if_snapshot_exists(self):
|
||||
bookmark = self.setup_bookmark(web_archive_snapshot_url='https://example.com')
|
||||
mock_cdx_api = create_cdx_server_api_mock()
|
||||
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI()) as mock_cdx_api:
|
||||
return_value=mock_cdx_api):
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
mock_cdx_api.assert_not_called()
|
||||
mock_cdx_api.newest.assert_not_called()
|
||||
|
||||
def test_load_web_archive_snapshot_should_handle_missing_snapshot(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_cdx_api = create_cdx_server_api_mock(archive_url=None)
|
||||
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(has_no_snapshot=True)):
|
||||
return_value=mock_cdx_api):
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
@@ -196,14 +226,37 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_load_web_archive_snapshot_should_handle_wayback_errors(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_cdx_api = create_cdx_server_api_mock(fail_loading_snapshot=True)
|
||||
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=MockWaybackMachineCDXServerAPI(fail_loading_snapshot=True)):
|
||||
return_value=mock_cdx_api):
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
|
||||
self.assertEqual('', bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_load_web_archive_snapshot_should_not_save_stale_bookmark_data(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
mock_cdx_api = create_cdx_server_api_mock()
|
||||
|
||||
# update bookmark during API call to check that saving
|
||||
# the snapshot does not overwrite updated bookmark data
|
||||
def mock_newest_impl():
|
||||
bookmark.title = 'Updated title'
|
||||
bookmark.save()
|
||||
return mock.DEFAULT
|
||||
|
||||
mock_cdx_api.newest.side_effect = mock_newest_impl
|
||||
|
||||
with mock.patch.object(bookmarks.services.wayback, 'CustomWaybackMachineCDXServerAPI',
|
||||
return_value=mock_cdx_api):
|
||||
tasks._load_web_archive_snapshot_task(bookmark.id)
|
||||
self.run_pending_task(tasks._load_web_archive_snapshot_task)
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertEqual('Updated title', bookmark.title)
|
||||
self.assertEqual('https://example.com/newest_snapshot', bookmark.web_archive_snapshot_url)
|
||||
|
||||
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
|
||||
def test_create_web_archive_snapshot_should_not_run_when_background_tasks_are_disabled(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
@@ -298,6 +351,26 @@ class BookmarkTasksTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
mock_load_favicon.assert_not_called()
|
||||
|
||||
def test_load_favicon_should_not_save_stale_bookmark_data(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
# update bookmark during API call to check that saving
|
||||
# the favicon does not overwrite updated bookmark data
|
||||
def mock_load_favicon_impl(url):
|
||||
bookmark.title = 'Updated title'
|
||||
bookmark.save()
|
||||
return 'https_example_com.png'
|
||||
|
||||
with mock.patch('bookmarks.services.favicon_loader.load_favicon') as mock_load_favicon:
|
||||
mock_load_favicon.side_effect = mock_load_favicon_impl
|
||||
|
||||
tasks.load_favicon(self.get_or_create_test_user(), bookmark)
|
||||
self.run_pending_task(tasks._load_favicon_task)
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertEqual(bookmark.title, 'Updated title')
|
||||
self.assertEqual(bookmark.favicon_file, 'https_example_com.png')
|
||||
|
||||
@override_settings(LD_DISABLE_BACKGROUND_TASKS=True)
|
||||
def test_load_favicon_should_not_run_when_background_tasks_are_disabled(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
28
bookmarks/tests/test_exporter.py
Normal file
28
bookmarks/tests/test_exporter.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from bookmarks.services import exporter
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class ExporterTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_escape_html_in_title_and_description(self):
|
||||
bookmark = self.setup_bookmark(
|
||||
title='<style>: The Style Information element',
|
||||
description='The <style> HTML element contains style information for a document, or part of a document.'
|
||||
)
|
||||
html = exporter.export_netscape_html([bookmark])
|
||||
|
||||
self.assertIn('<style>: The Style Information element', html)
|
||||
self.assertIn(
|
||||
'The <style> HTML element contains style information for a document, or part of a document.',
|
||||
html
|
||||
)
|
||||
|
||||
def test_handle_empty_values(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.title = ''
|
||||
bookmark.description = ''
|
||||
bookmark.website_title = None
|
||||
bookmark.website_description = None
|
||||
bookmark.save()
|
||||
exporter.export_netscape_html([bookmark])
|
@@ -5,7 +5,7 @@ from django.db.models import QuerySet
|
||||
from django.test import TestCase
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks.models import Bookmark
|
||||
from bookmarks.models import Bookmark, UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, random_sentence
|
||||
from bookmarks.utils import unique
|
||||
|
||||
@@ -13,6 +13,8 @@ User = get_user_model()
|
||||
|
||||
|
||||
class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def setUp(self):
|
||||
self.profile = self.get_or_create_test_user().profile
|
||||
|
||||
def setup_bookmark_search_data(self) -> None:
|
||||
tag1 = self.setup_tag(name='tag1')
|
||||
@@ -27,9 +29,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.term1_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1'),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1')),
|
||||
self.setup_bookmark(title=random_sentence(including_word='TERM1')),
|
||||
self.setup_bookmark(description=random_sentence(including_word='term1')),
|
||||
self.setup_bookmark(description=random_sentence(including_word='TERM1')),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='term1')),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='TERM1')),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='term1')),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='TERM1')),
|
||||
]
|
||||
self.term1_term2_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1/term2'),
|
||||
@@ -49,6 +55,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(website_title=random_sentence(), tags=[tag1]),
|
||||
self.setup_bookmark(website_description=random_sentence(), tags=[tag1]),
|
||||
]
|
||||
self.tag1_as_term_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/tag1'),
|
||||
self.setup_bookmark(title=random_sentence(including_word='tag1')),
|
||||
self.setup_bookmark(description=random_sentence(including_word='tag1')),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='tag1')),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='tag1')),
|
||||
]
|
||||
self.term1_tag1_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1', tags=[tag1]),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1]),
|
||||
@@ -76,9 +89,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.term1_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1', tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(title=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(description=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(description=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
|
||||
]
|
||||
self.term1_term2_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1/term2', tags=[self.setup_tag()]),
|
||||
@@ -102,6 +119,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(website_title=random_sentence(), tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(website_description=random_sentence(), tags=[tag1, self.setup_tag()]),
|
||||
]
|
||||
self.tag1_as_term_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/tag1'),
|
||||
self.setup_bookmark(title=random_sentence(including_word='tag1')),
|
||||
self.setup_bookmark(description=random_sentence(including_word='tag1')),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='tag1')),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='tag1')),
|
||||
]
|
||||
self.term1_tag1_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1', tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1, self.setup_tag()]),
|
||||
@@ -135,12 +159,13 @@ 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.get_or_create_test_user(), '')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '')
|
||||
self.assertQueryResult(query, [
|
||||
self.other_bookmarks,
|
||||
self.term1_bookmarks,
|
||||
self.term1_term2_bookmarks,
|
||||
self.tag1_bookmarks,
|
||||
self.tag1_as_term_bookmarks,
|
||||
self.term1_tag1_bookmarks,
|
||||
self.tag2_bookmarks,
|
||||
self.tag1_tag2_bookmarks
|
||||
@@ -149,7 +174,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_query_bookmarks_should_search_single_term(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1')
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'term1')
|
||||
self.assertQueryResult(query, [
|
||||
self.term1_bookmarks,
|
||||
self.term1_term2_bookmarks,
|
||||
@@ -159,63 +184,101 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_query_bookmarks_should_search_multiple_terms(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term2 term1')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '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.get_or_create_test_user(), '#tag1')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '#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.get_or_create_test_user(), '#tag1 #tag2')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '#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.get_or_create_test_user(), '#Tag1 #TAG2')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '#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.get_or_create_test_user(), 'term1 #tag1')
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'term1 #tag1')
|
||||
|
||||
self.assertQueryResult(query, [self.term1_tag1_bookmarks])
|
||||
|
||||
def test_query_bookmarks_in_strict_mode_should_not_search_tags_as_terms(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
self.profile.tag_search = UserProfile.TAG_SEARCH_STRICT
|
||||
self.profile.save()
|
||||
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'tag1')
|
||||
self.assertQueryResult(query, [self.tag1_as_term_bookmarks])
|
||||
|
||||
def test_query_bookmarks_in_lax_mode_should_search_tags_as_terms(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
self.profile.tag_search = UserProfile.TAG_SEARCH_LAX
|
||||
self.profile.save()
|
||||
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'tag1')
|
||||
self.assertQueryResult(query, [
|
||||
self.tag1_bookmarks,
|
||||
self.tag1_as_term_bookmarks,
|
||||
self.tag1_tag2_bookmarks,
|
||||
self.term1_tag1_bookmarks
|
||||
])
|
||||
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'tag1 term1')
|
||||
self.assertQueryResult(query, [
|
||||
self.term1_tag1_bookmarks,
|
||||
])
|
||||
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'tag1 tag2')
|
||||
self.assertQueryResult(query, [
|
||||
self.tag1_tag2_bookmarks,
|
||||
])
|
||||
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'tag1 #tag2')
|
||||
self.assertQueryResult(query, [
|
||||
self.tag1_tag2_bookmarks,
|
||||
])
|
||||
|
||||
def test_query_bookmarks_should_return_no_matches(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term3')
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'term3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1 term3')
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'term1 term3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1 #tag2')
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'term1 #tag2')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#tag3')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '#tag3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#unused_tag1')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '#unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag combined with tag that is used
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#tag1 #unused_tag1')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '#tag1 #unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag combined with term that is used
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1 #unused_tag1')
|
||||
query = queries.query_bookmarks(self.user, self.profile, 'term1 #unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
def test_query_bookmarks_should_not_return_archived_bookmarks(self):
|
||||
@@ -225,7 +288,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(is_archived=True)
|
||||
self.setup_bookmark(is_archived=True)
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [[bookmark1, bookmark2]])
|
||||
|
||||
@@ -236,7 +299,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
|
||||
query = queries.query_archived_bookmarks(self.get_or_create_test_user(), '')
|
||||
query = queries.query_archived_bookmarks(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [[bookmark1, bookmark2]])
|
||||
|
||||
@@ -251,7 +314,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(user=other_user)
|
||||
self.setup_bookmark(user=other_user)
|
||||
|
||||
query = queries.query_bookmarks(self.user, '')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [owned_bookmarks])
|
||||
|
||||
@@ -266,7 +329,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, '')
|
||||
query = queries.query_archived_bookmarks(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [owned_bookmarks])
|
||||
|
||||
@@ -276,7 +339,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(tags=[tag])
|
||||
self.setup_bookmark(tags=[tag])
|
||||
|
||||
query = queries.query_bookmarks(self.user, '!untagged')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '!untagged')
|
||||
self.assertCountEqual(list(query), [untagged_bookmark])
|
||||
|
||||
def test_query_bookmarks_untagged_should_be_combinable_with_search_terms(self):
|
||||
@@ -285,7 +348,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(title='term2')
|
||||
self.setup_bookmark(tags=[tag])
|
||||
|
||||
query = queries.query_bookmarks(self.user, '!untagged term1')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '!untagged term1')
|
||||
self.assertCountEqual(list(query), [untagged_bookmark])
|
||||
|
||||
def test_query_bookmarks_untagged_should_not_be_combinable_with_tags(self):
|
||||
@@ -294,7 +357,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(tags=[tag])
|
||||
self.setup_bookmark(tags=[tag])
|
||||
|
||||
query = queries.query_bookmarks(self.user, f'!untagged #{tag.name}')
|
||||
query = queries.query_bookmarks(self.user, self.profile, f'!untagged #{tag.name}')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
def test_query_archived_bookmarks_untagged_should_return_untagged_bookmarks_only(self):
|
||||
@@ -303,7 +366,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, '!untagged')
|
||||
query = queries.query_archived_bookmarks(self.user, self.profile, '!untagged')
|
||||
self.assertCountEqual(list(query), [untagged_bookmark])
|
||||
|
||||
def test_query_archived_bookmarks_untagged_should_be_combinable_with_search_terms(self):
|
||||
@@ -312,7 +375,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, '!untagged term1')
|
||||
query = queries.query_archived_bookmarks(self.user, self.profile, '!untagged term1')
|
||||
self.assertCountEqual(list(query), [untagged_bookmark])
|
||||
|
||||
def test_query_archived_bookmarks_untagged_should_not_be_combinable_with_tags(self):
|
||||
@@ -321,7 +384,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, f'!untagged #{tag.name}')
|
||||
query = queries.query_archived_bookmarks(self.user, self.profile, f'!untagged #{tag.name}')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
def test_query_bookmarks_unread_should_return_unread_bookmarks_only(self):
|
||||
@@ -334,7 +397,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark()
|
||||
|
||||
query = queries.query_bookmarks(self.user, '!unread')
|
||||
query = queries.query_bookmarks(self.user, self.profile, '!unread')
|
||||
self.assertCountEqual(list(query), unread_bookmarks)
|
||||
|
||||
def test_query_archived_bookmarks_unread_should_return_unread_bookmarks_only(self):
|
||||
@@ -347,13 +410,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(is_archived=True)
|
||||
self.setup_bookmark(is_archived=True)
|
||||
|
||||
query = queries.query_archived_bookmarks(self.user, '!unread')
|
||||
query = queries.query_archived_bookmarks(self.user, self.profile, '!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, '')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.other_bookmarks),
|
||||
@@ -368,7 +431,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, 'term1')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'term1')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.term1_bookmarks),
|
||||
@@ -379,7 +442,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, 'term2 term1')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'term2 term1')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
|
||||
@@ -388,7 +451,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, '#tag1')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '#tag1')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_bookmarks),
|
||||
@@ -399,7 +462,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, '#tag1 #tag2')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '#tag1 #tag2')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
@@ -408,7 +471,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, '#Tag1 #TAG2')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '#Tag1 #TAG2')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
@@ -417,37 +480,75 @@ 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, 'term1 #tag1')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'term1 #tag1')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_in_strict_mode_should_not_search_tags_as_terms(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
self.profile.tag_search = UserProfile.TAG_SEARCH_STRICT
|
||||
self.profile.save()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '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):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
self.profile.tag_search = UserProfile.TAG_SEARCH_LAX
|
||||
self.profile.save()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'tag1')
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.tag1_as_term_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.term1_tag1_bookmarks)
|
||||
])
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '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')
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
])
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'tag1 #tag2')
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_should_return_no_matches(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), 'term3')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'term3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), 'term1 term3')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'term1 term3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), 'term1 #tag2')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'term1 #tag2')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '#tag3')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '#tag3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '#unused_tag1')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '#unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag combined with tag that is used
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '#tag1 #unused_tag1')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '#tag1 #unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag combined with term that is used
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), 'term1 #unused_tag1')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, 'term1 #unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
def test_query_bookmark_tags_should_return_tags_for_unarchived_bookmarks_only(self):
|
||||
@@ -457,7 +558,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark(is_archived=True, tags=[tag2])
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [[tag1]])
|
||||
|
||||
@@ -467,7 +568,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(tags=[tag])
|
||||
self.setup_bookmark(tags=[tag])
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [[tag]])
|
||||
|
||||
@@ -478,7 +579,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark()
|
||||
self.setup_bookmark(is_archived=True, tags=[tag2])
|
||||
|
||||
query = queries.query_archived_bookmark_tags(self.get_or_create_test_user(), '')
|
||||
query = queries.query_archived_bookmark_tags(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [[tag2]])
|
||||
|
||||
@@ -488,7 +589,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.get_or_create_test_user(), '')
|
||||
query = queries.query_archived_bookmark_tags(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [[tag]])
|
||||
|
||||
@@ -503,7 +604,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, '')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
|
||||
|
||||
@@ -518,7 +619,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, '')
|
||||
query = queries.query_archived_bookmark_tags(self.user, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
|
||||
|
||||
@@ -529,13 +630,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.setup_bookmark(title='term1', tags=[tag])
|
||||
self.setup_bookmark(tags=[tag])
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, '!untagged')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '!untagged')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, '!untagged term1')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, '!untagged term1')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, f'!untagged #{tag.name}')
|
||||
query = queries.query_bookmark_tags(self.user, self.profile, f'!untagged #{tag.name}')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
def test_query_archived_bookmark_tags_untagged_should_never_return_any_tags(self):
|
||||
@@ -545,13 +646,13 @@ 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, '!untagged')
|
||||
query = queries.query_archived_bookmark_tags(self.user, self.profile, '!untagged')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
query = queries.query_archived_bookmark_tags(self.user, '!untagged term1')
|
||||
query = queries.query_archived_bookmark_tags(self.user, self.profile, '!untagged term1')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
query = queries.query_archived_bookmark_tags(self.user, f'!untagged #{tag.name}')
|
||||
query = queries.query_archived_bookmark_tags(self.user, self.profile, f'!untagged #{tag.name}')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
def test_query_shared_bookmarks(self):
|
||||
@@ -574,14 +675,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, '')
|
||||
query_set = queries.query_shared_bookmarks(None, self.profile, '')
|
||||
self.assertQueryResult(query_set, [shared_bookmarks])
|
||||
|
||||
# Should respect search query
|
||||
query_set = queries.query_shared_bookmarks(None, 'test title')
|
||||
query_set = queries.query_shared_bookmarks(None, self.profile, 'test title')
|
||||
self.assertQueryResult(query_set, [[shared_bookmarks[0]]])
|
||||
|
||||
query_set = queries.query_shared_bookmarks(None, '#' + tag.name)
|
||||
query_set = queries.query_shared_bookmarks(None, self.profile, '#' + tag.name)
|
||||
self.assertQueryResult(query_set, [[shared_bookmarks[2]]])
|
||||
|
||||
def test_query_shared_bookmark_tags(self):
|
||||
@@ -605,7 +706,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, '')
|
||||
query_set = queries.query_shared_bookmark_tags(None, self.profile, '')
|
||||
|
||||
self.assertQueryResult(query_set, [shared_tags])
|
||||
|
||||
@@ -630,9 +731,9 @@ 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('')
|
||||
query_set = queries.query_shared_bookmark_users(self.profile, '')
|
||||
self.assertQueryResult(query_set, [users_with_shared_bookmarks])
|
||||
|
||||
# Should respect search query
|
||||
query_set = queries.query_shared_bookmark_users('test title')
|
||||
query_set = queries.query_shared_bookmark_users(self.profile, 'test title')
|
||||
self.assertQueryResult(query_set, [[users_with_shared_bookmarks[0]]])
|
||||
|
@@ -28,6 +28,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED,
|
||||
'enable_sharing': False,
|
||||
'enable_favicons': False,
|
||||
'tag_search': UserProfile.TAG_SEARCH_STRICT,
|
||||
'display_url': False,
|
||||
}
|
||||
|
||||
return {**form_data, **overrides}
|
||||
@@ -52,6 +54,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
|
||||
'enable_sharing': True,
|
||||
'enable_favicons': True,
|
||||
'tag_search': UserProfile.TAG_SEARCH_LAX,
|
||||
'display_url': True,
|
||||
}
|
||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
html = response.content.decode()
|
||||
@@ -65,6 +69,8 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(self.user.profile.web_archive_integration, form_data['web_archive_integration'])
|
||||
self.assertEqual(self.user.profile.enable_sharing, form_data['enable_sharing'])
|
||||
self.assertEqual(self.user.profile.enable_favicons, form_data['enable_favicons'])
|
||||
self.assertEqual(self.user.profile.tag_search, form_data['tag_search'])
|
||||
self.assertEqual(self.user.profile.display_url, form_data['display_url'])
|
||||
self.assertInHTML('''
|
||||
<p class="form-input-hint">Profile updated</p>
|
||||
''', html)
|
||||
|
@@ -3,7 +3,7 @@ from typing import List
|
||||
from django.template import Template, RequestContext
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
from bookmarks.models import Tag
|
||||
from bookmarks.models import Tag, UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
request.user = self.get_or_create_test_user()
|
||||
context = RequestContext(request, {
|
||||
'request': request,
|
||||
'tags': tags,
|
||||
@@ -118,6 +119,36 @@ class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
</a>
|
||||
''', rendered_template)
|
||||
|
||||
def test_selected_tags_with_lax_tag_search(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
profile.tag_search = UserProfile.TAG_SEARCH_LAX
|
||||
profile.save()
|
||||
|
||||
tags = [
|
||||
self.setup_tag(name='tag1'),
|
||||
self.setup_tag(name='tag2'),
|
||||
]
|
||||
|
||||
# Filter by tag name without hash
|
||||
rendered_template = self.render_template(tags, tags, url='/test?q=tag1 %23tag2')
|
||||
|
||||
self.assertNumSelectedTags(rendered_template, 2)
|
||||
|
||||
# Tag name should still be removed from query string
|
||||
self.assertInHTML('''
|
||||
<a href="?q=%23tag2"
|
||||
class="text-bold mr-2">
|
||||
<span>-tag1</span>
|
||||
</a>
|
||||
''', rendered_template)
|
||||
|
||||
self.assertInHTML('''
|
||||
<a href="?q=tag1"
|
||||
class="text-bold mr-2">
|
||||
<span>-tag2</span>
|
||||
</a>
|
||||
''', rendered_template)
|
||||
|
||||
def test_selected_tags_ignore_casing_when_removing_query_part(self):
|
||||
tags = [
|
||||
self.setup_tag(name='TEST'),
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import urllib.parse
|
||||
from typing import List
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
@@ -9,7 +10,7 @@ from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks.models import Bookmark, BookmarkForm, BookmarkFilters, User, Tag, build_tag_string
|
||||
from bookmarks.models import Bookmark, BookmarkForm, BookmarkFilters, User, UserProfile, Tag, 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
|
||||
from bookmarks.utils import get_safe_return_url
|
||||
@@ -20,8 +21,8 @@ _default_page_size = 30
|
||||
@login_required
|
||||
def index(request):
|
||||
filters = BookmarkFilters(request)
|
||||
query_set = queries.query_bookmarks(request.user, filters.query)
|
||||
tags = queries.query_bookmark_tags(request.user, filters.query)
|
||||
query_set = queries.query_bookmarks(request.user, request.user.profile, filters.query)
|
||||
tags = queries.query_bookmark_tags(request.user, request.user.profile, filters.query)
|
||||
base_url = reverse('bookmarks:index')
|
||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||
return render(request, 'bookmarks/index.html', context)
|
||||
@@ -30,8 +31,8 @@ def index(request):
|
||||
@login_required
|
||||
def archived(request):
|
||||
filters = BookmarkFilters(request)
|
||||
query_set = queries.query_archived_bookmarks(request.user, filters.query)
|
||||
tags = queries.query_archived_bookmark_tags(request.user, filters.query)
|
||||
query_set = queries.query_archived_bookmarks(request.user, request.user.profile, filters.query)
|
||||
tags = queries.query_archived_bookmark_tags(request.user, request.user.profile, filters.query)
|
||||
base_url = reverse('bookmarks:archived')
|
||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||
return render(request, 'bookmarks/archive.html', context)
|
||||
@@ -41,27 +42,23 @@ def archived(request):
|
||||
def shared(request):
|
||||
filters = BookmarkFilters(request)
|
||||
user = User.objects.filter(username=filters.user).first()
|
||||
query_set = queries.query_shared_bookmarks(user, filters.query)
|
||||
tags = queries.query_shared_bookmark_tags(user, filters.query)
|
||||
users = queries.query_shared_bookmark_users(filters.query)
|
||||
query_set = queries.query_shared_bookmarks(user, request.user.profile, filters.query)
|
||||
tags = queries.query_shared_bookmark_tags(user, request.user.profile, filters.query)
|
||||
users = queries.query_shared_bookmark_users(request.user.profile, filters.query)
|
||||
base_url = reverse('bookmarks:shared')
|
||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||
context['users'] = users
|
||||
return render(request, 'bookmarks/shared.html', context)
|
||||
|
||||
|
||||
def _get_selected_tags(tags: QuerySet[Tag], query_string: str):
|
||||
def _get_selected_tags(tags: List[Tag], query_string: str, profile: UserProfile):
|
||||
parsed_query = queries.parse_query_string(query_string)
|
||||
tag_names = parsed_query['tag_names']
|
||||
if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
|
||||
tag_names = tag_names + parsed_query['search_terms']
|
||||
tag_names = [tag_name.lower() for tag_name in tag_names]
|
||||
|
||||
if len(tag_names) == 0:
|
||||
return []
|
||||
|
||||
condition = Q()
|
||||
for tag_name in parsed_query['tag_names']:
|
||||
condition = condition | Q(name__iexact=tag_name)
|
||||
|
||||
return list(tags.filter(condition))
|
||||
return [tag for tag in tags if tag.name.lower() in tag_names]
|
||||
|
||||
|
||||
def get_bookmark_view_context(request: WSGIRequest,
|
||||
@@ -72,7 +69,8 @@ def get_bookmark_view_context(request: WSGIRequest,
|
||||
page = request.GET.get('page')
|
||||
paginator = Paginator(query_set, _default_page_size)
|
||||
bookmarks = paginator.get_page(page)
|
||||
selected_tags = _get_selected_tags(tags, filters.query)
|
||||
tags = list(tags)
|
||||
selected_tags = _get_selected_tags(tags, filters.query, request.user.profile)
|
||||
# Prefetch related objects, this avoids n+1 queries when accessing fields in templates
|
||||
prefetch_related_objects(bookmarks.object_list, 'owner', 'tags')
|
||||
return_url = generate_return_url(base_url, page, filters)
|
||||
|
@@ -141,7 +141,7 @@ def bookmark_import(request):
|
||||
def bookmark_export(request):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
bookmarks = list(query_bookmarks(request.user, ''))
|
||||
bookmarks = list(query_bookmarks(request.user, request.user.profile, ''))
|
||||
# Prefetch tags to prevent n+1 queries
|
||||
prefetch_related_objects(bookmarks, 'tags')
|
||||
file_content = exporter.export_netscape_html(bookmarks)
|
||||
|
@@ -109,6 +109,12 @@ Multiple origins can be specified by separating them with a comma (`,`).
|
||||
|
||||
This setting is adopted from the Django framework used by linkding, more information on the setting is available in the [Django documentation](https://docs.djangoproject.com/en/4.0/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS).
|
||||
|
||||
### `LD_LOG_X_FORWARDED_FOR`
|
||||
|
||||
Values: `true` or `false` | Default = `false`
|
||||
|
||||
Set uWSGI [log-x-forwarded-for](https://uwsgi-docs.readthedocs.io/en/latest/Options.html?#log-x-forwarded-for) parameter allowing to keep the real IP of clients in logs when using a reverse proxy.
|
||||
|
||||
### `LD_DB_ENGINE`
|
||||
|
||||
Values: `postgres` or `sqlite` | Default = `sqlite`
|
||||
@@ -150,6 +156,12 @@ Values: `Integer` | Default = None
|
||||
The port of the database server.
|
||||
Should use the default port if left empty, for example `5432` for PostgresSQL.
|
||||
|
||||
### `LD_DB_OPTIONS`
|
||||
|
||||
Values: `String` | Default = `{}`
|
||||
|
||||
A json string with additional options for the database. Passed directly to OPTIONS.
|
||||
|
||||
### `LD_FAVICON_PROVIDER`
|
||||
|
||||
Values: `String` | Default = `https://t1.gstatic.com/faviconV2?url={url}&client=SOCIAL&type=FAVICON`
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkding",
|
||||
"version": "1.17.0",
|
||||
"version": "1.18.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@@ -4,7 +4,7 @@ certifi==2022.12.7
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
confusable-homoglyphs==3.2.0
|
||||
Django==4.1.2
|
||||
Django==4.1.9
|
||||
django-generate-secret-key==1.0.2
|
||||
django-registration==3.3
|
||||
django-sass-processor==1.2.1
|
||||
@@ -17,7 +17,7 @@ python-dateutil==2.8.2
|
||||
pytz==2022.2.1
|
||||
requests==2.28.1
|
||||
soupsieve==2.3.2.post1
|
||||
sqlparse==0.4.2
|
||||
sqlparse==0.4.4
|
||||
supervisor==4.2.4
|
||||
typing-extensions==3.10.0.0
|
||||
urllib3==1.26.11
|
||||
|
@@ -5,7 +5,7 @@ charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
confusable-homoglyphs==3.2.0
|
||||
coverage==5.5
|
||||
Django==4.1.2
|
||||
Django==4.1.9
|
||||
django-appconf==1.0.5
|
||||
django-compressor==4.1
|
||||
django-debug-toolbar==3.6.0
|
||||
@@ -15,9 +15,12 @@ django-sass-processor==1.2.1
|
||||
django-widget-tweaks==1.4.12
|
||||
django4-background-tasks==1.2.7
|
||||
djangorestframework==3.13.1
|
||||
greenlet==2.0.1
|
||||
idna==3.3
|
||||
libsass==0.21.0
|
||||
playwright==1.29.1
|
||||
psycopg2-binary==2.9.5
|
||||
pyee==9.0.4
|
||||
python-dateutil==2.8.2
|
||||
pytz==2022.2.1
|
||||
rcssmin==1.1.0
|
||||
@@ -25,7 +28,7 @@ requests==2.28.1
|
||||
rjsmin==1.2.0
|
||||
six==1.16.0
|
||||
soupsieve==2.3.2.post1
|
||||
sqlparse==0.4.2
|
||||
sqlparse==0.4.4
|
||||
typing-extensions==3.10.0.0
|
||||
urllib3==1.26.11
|
||||
waybackpy==3.0.6
|
||||
|
@@ -10,6 +10,7 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
@@ -204,6 +205,7 @@ LD_DB_DATABASE = os.getenv('LD_DB_DATABASE', 'linkding')
|
||||
LD_DB_USER = os.getenv('LD_DB_USER', 'linkding')
|
||||
LD_DB_PASSWORD = os.getenv('LD_DB_PASSWORD', None)
|
||||
LD_DB_PORT = os.getenv('LD_DB_PORT', None)
|
||||
LD_DB_OPTIONS = json.loads(os.getenv('LD_DB_OPTIONS') or '{}')
|
||||
|
||||
if LD_DB_ENGINE == 'postgres':
|
||||
default_database = {
|
||||
@@ -213,11 +215,13 @@ if LD_DB_ENGINE == 'postgres':
|
||||
'PASSWORD': LD_DB_PASSWORD,
|
||||
'HOST': LD_DB_HOST,
|
||||
'PORT': LD_DB_PORT,
|
||||
'OPTIONS': LD_DB_OPTIONS,
|
||||
}
|
||||
else:
|
||||
default_database = {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'data', 'db.sqlite3'),
|
||||
'OPTIONS': LD_DB_OPTIONS,
|
||||
}
|
||||
|
||||
DATABASES = {
|
||||
|
@@ -22,4 +22,8 @@ if-env = LD_REQUEST_TIMEOUT
|
||||
http-timeout = %(_)
|
||||
socket-timeout = %(_)
|
||||
harakiri = %(_)
|
||||
endif =
|
||||
endif =
|
||||
|
||||
if-env = LD_LOG_X_FORWARDED_FOR
|
||||
log-x-forwarded-for = %(_)
|
||||
endif =
|
||||
|
@@ -1 +1 @@
|
||||
1.17.0
|
||||
1.18.0
|
||||
|
Reference in New Issue
Block a user