mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-03-10 20:03:12 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
047d3be1b5 | ||
|
|
43115fd8f2 | ||
|
|
67ee896a46 | ||
|
|
fd3070c6f3 | ||
|
|
bc374e90a2 | ||
|
|
a94eb5f85a | ||
|
|
d1819c6503 | ||
|
|
353ba433f0 | ||
|
|
3af4e07eb6 | ||
|
|
e9061f373a | ||
|
|
f87398742a | ||
|
|
81dc19958c | ||
|
|
5049ff14cf | ||
|
|
f9ab3d1f44 |
@@ -45,3 +45,5 @@ LD_DB_HOST=
|
|||||||
# Port use to connect to the database server
|
# Port use to connect to the database server
|
||||||
# Should use the default port if not set
|
# Should use the default port if not set
|
||||||
LD_DB_PORT=
|
LD_DB_PORT=
|
||||||
|
# Any additional options to pass to the database (default: {})
|
||||||
|
LD_DB_OPTIONS=
|
||||||
|
|||||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,5 +1,45 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.18.0 (18/05/2023)
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
* Make search case-insensitive on Postgres by @sissbruecker in https://github.com/sissbruecker/linkding/pull/432
|
||||||
|
* Allow searching for tags without hash character by @sissbruecker in https://github.com/sissbruecker/linkding/pull/449
|
||||||
|
* Prevent zoom-in after focusing an input on small viewports on iOS devices by @puresick in https://github.com/sissbruecker/linkding/pull/440
|
||||||
|
* Add database options by @plockaby in https://github.com/sissbruecker/linkding/pull/406
|
||||||
|
* Allow to log real client ip in logs when using a reverse proxy by @fmenabe in https://github.com/sissbruecker/linkding/pull/398
|
||||||
|
* Add option to display URL below title by @bah0 in https://github.com/sissbruecker/linkding/pull/365
|
||||||
|
* Add LinkThing iOS app to community section by @amoscardino in https://github.com/sissbruecker/linkding/pull/446
|
||||||
|
* Bump django from 4.1.7 to 4.1.9 by @dependabot in https://github.com/sissbruecker/linkding/pull/466
|
||||||
|
* Bump sqlparse from 0.4.2 to 0.4.4 by @dependabot in https://github.com/sissbruecker/linkding/pull/455
|
||||||
|
|
||||||
|
### New Contributors
|
||||||
|
* @amoscardino made their first contribution in https://github.com/sissbruecker/linkding/pull/446
|
||||||
|
* @puresick made their first contribution in https://github.com/sissbruecker/linkding/pull/440
|
||||||
|
* @plockaby made their first contribution in https://github.com/sissbruecker/linkding/pull/406
|
||||||
|
* @fmenabe made their first contribution in https://github.com/sissbruecker/linkding/pull/398
|
||||||
|
* @bah0 made their first contribution in https://github.com/sissbruecker/linkding/pull/365
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.17.2...v1.18.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
## v1.17.1 (22/01/2023)
|
||||||
|
|
||||||
### What's Changed
|
### What's Changed
|
||||||
|
|||||||
@@ -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)
|
- [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)
|
- [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)
|
- [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
|
## Acknowledgements
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
|||||||
# For list action, use query set that applies search and tag projections
|
# For list action, use query set that applies search and tag projections
|
||||||
if self.action == 'list':
|
if self.action == 'list':
|
||||||
query_string = self.request.GET.get('q')
|
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
|
# For single entity actions use default query set without projections
|
||||||
return Bookmark.objects.all().filter(owner=user)
|
return Bookmark.objects.all().filter(owner=user)
|
||||||
@@ -35,7 +35,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
|||||||
def archived(self, request):
|
def archived(self, request):
|
||||||
user = request.user
|
user = request.user
|
||||||
query_string = request.GET.get('q')
|
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)
|
page = self.paginate_queryset(query_set)
|
||||||
serializer = self.get_serializer_class()
|
serializer = self.get_serializer_class()
|
||||||
data = serializer(page, many=True).data
|
data = serializer(page, many=True).data
|
||||||
@@ -45,7 +45,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
|||||||
def shared(self, request):
|
def shared(self, request):
|
||||||
filters = BookmarkFilters(request)
|
filters = BookmarkFilters(request)
|
||||||
user = User.objects.filter(username=filters.user).first()
|
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)
|
page = self.paginate_queryset(query_set)
|
||||||
serializer = self.get_serializer_class()
|
serializer = self.get_serializer_class()
|
||||||
data = serializer(page, many=True).data
|
data = serializer(page, many=True).data
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||||||
'url',
|
'url',
|
||||||
'title',
|
'title',
|
||||||
'description',
|
'description',
|
||||||
|
'notes',
|
||||||
'website_title',
|
'website_title',
|
||||||
'website_description',
|
'website_description',
|
||||||
'is_archived',
|
'is_archived',
|
||||||
@@ -47,6 +48,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||||||
# Override optional char fields to provide default value
|
# Override optional char fields to provide default value
|
||||||
title = serializers.CharField(required=False, allow_blank=True, default='')
|
title = serializers.CharField(required=False, allow_blank=True, default='')
|
||||||
description = serializers.CharField(required=False, allow_blank=True, default='')
|
description = serializers.CharField(required=False, allow_blank=True, default='')
|
||||||
|
notes = serializers.CharField(required=False, allow_blank=True, default='')
|
||||||
is_archived = serializers.BooleanField(required=False, default=False)
|
is_archived = serializers.BooleanField(required=False, default=False)
|
||||||
unread = serializers.BooleanField(required=False, default=False)
|
unread = serializers.BooleanField(required=False, default=False)
|
||||||
shared = serializers.BooleanField(required=False, default=False)
|
shared = serializers.BooleanField(required=False, default=False)
|
||||||
@@ -58,6 +60,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||||||
bookmark.url = validated_data['url']
|
bookmark.url = validated_data['url']
|
||||||
bookmark.title = validated_data['title']
|
bookmark.title = validated_data['title']
|
||||||
bookmark.description = validated_data['description']
|
bookmark.description = validated_data['description']
|
||||||
|
bookmark.notes = validated_data['notes']
|
||||||
bookmark.is_archived = validated_data['is_archived']
|
bookmark.is_archived = validated_data['is_archived']
|
||||||
bookmark.unread = validated_data['unread']
|
bookmark.unread = validated_data['unread']
|
||||||
bookmark.shared = validated_data['shared']
|
bookmark.shared = validated_data['shared']
|
||||||
@@ -66,7 +69,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def update(self, instance: Bookmark, validated_data):
|
def update(self, instance: Bookmark, validated_data):
|
||||||
# Update fields if they were provided in the payload
|
# Update fields if they were provided in the payload
|
||||||
for key in ['url', 'title', 'description', 'unread', 'shared']:
|
for key in ['url', 'title', 'description', 'notes', 'unread', 'shared']:
|
||||||
if key in validated_data:
|
if key in validated_data:
|
||||||
setattr(instance, key, validated_data[key])
|
setattr(instance, key, validated_data[key])
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright, expect
|
||||||
|
|
||||||
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
|||||||
def test_create_should_check_for_existing_bookmark(self):
|
def test_create_should_check_for_existing_bookmark(self):
|
||||||
existing_bookmark = self.setup_bookmark(title='Existing title',
|
existing_bookmark = self.setup_bookmark(title='Existing title',
|
||||||
description='Existing description',
|
description='Existing description',
|
||||||
|
notes='Existing notes',
|
||||||
tags=[self.setup_tag(name='tag1'), self.setup_tag(name='tag2')],
|
tags=[self.setup_tag(name='tag1'), self.setup_tag(name='tag2')],
|
||||||
website_title='Existing website title',
|
website_title='Existing website title',
|
||||||
website_description='Existing website description',
|
website_description='Existing website description',
|
||||||
@@ -26,6 +27,7 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
|||||||
# Form should be pre-filled with data from existing bookmark
|
# 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.title, page.get_by_label('Title').input_value())
|
||||||
self.assertEqual(existing_bookmark.description, page.get_by_label('Description').input_value())
|
self.assertEqual(existing_bookmark.description, page.get_by_label('Description').input_value())
|
||||||
|
self.assertEqual(existing_bookmark.notes, page.get_by_label('Notes').input_value())
|
||||||
self.assertEqual(existing_bookmark.website_title, page.get_by_label('Title').get_attribute('placeholder'))
|
self.assertEqual(existing_bookmark.website_title, page.get_by_label('Title').get_attribute('placeholder'))
|
||||||
self.assertEqual(existing_bookmark.website_description,
|
self.assertEqual(existing_bookmark.website_description,
|
||||||
page.get_by_label('Description').get_attribute('placeholder'))
|
page.get_by_label('Description').get_attribute('placeholder'))
|
||||||
@@ -49,3 +51,17 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
|
|||||||
|
|
||||||
page.wait_for_timeout(timeout=1000)
|
page.wait_for_timeout(timeout=1000)
|
||||||
page.get_by_text('This URL is already bookmarked.').wait_for(state='hidden')
|
page.get_by_text('This URL is already bookmarked.').wait_for(state='hidden')
|
||||||
|
|
||||||
|
def test_enter_url_of_existing_bookmark_should_show_notes(self):
|
||||||
|
bookmark = self.setup_bookmark(notes='Existing notes', description='Existing description')
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = self.setup_browser(p)
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(self.live_server_url + reverse('bookmarks:new'))
|
||||||
|
|
||||||
|
details = page.locator('details.notes')
|
||||||
|
expect(details).not_to_have_attribute('open', value='')
|
||||||
|
|
||||||
|
page.get_by_label('URL').fill(bookmark.url)
|
||||||
|
expect(details).to_have_attribute('open', value='')
|
||||||
|
|||||||
27
bookmarks/e2e/test_bookmark_list.py
Normal file
27
bookmarks/e2e/test_bookmark_list.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from unittest import skip
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from playwright.sync_api import sync_playwright, expect
|
||||||
|
|
||||||
|
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||||
|
|
||||||
|
|
||||||
|
@skip("Fails in CI, needs investigation")
|
||||||
|
class BookmarkListE2ETestCase(LinkdingE2ETestCase):
|
||||||
|
def test_toggle_notes_should_show_hide_notes(self):
|
||||||
|
self.setup_bookmark(notes='Test notes')
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = self.setup_browser(p)
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(self.live_server_url + reverse('bookmarks:index'))
|
||||||
|
|
||||||
|
notes = page.locator('li .notes')
|
||||||
|
expect(notes).to_be_hidden()
|
||||||
|
|
||||||
|
toggle_notes = page.locator('li button.toggle-notes')
|
||||||
|
toggle_notes.click()
|
||||||
|
expect(notes).to_be_visible()
|
||||||
|
|
||||||
|
toggle_notes.click()
|
||||||
|
expect(notes).to_be_hidden()
|
||||||
@@ -18,7 +18,7 @@ class BaseBookmarksFeed(Feed):
|
|||||||
def get_object(self, request, feed_key: str):
|
def get_object(self, request, feed_key: str):
|
||||||
feed_token = FeedToken.objects.get(key__exact=feed_key)
|
feed_token = FeedToken.objects.get(key__exact=feed_key)
|
||||||
query_string = request.GET.get('q')
|
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)
|
return FeedContext(feed_token, query_set)
|
||||||
|
|
||||||
def item_title(self, item: Bookmark):
|
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
bookmarks/migrations/0022_bookmark_notes.py
Normal file
18
bookmarks/migrations/0022_bookmark_notes.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-05-19 10:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bookmarks', '0021_userprofile_display_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bookmark',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
bookmarks/migrations/0023_userprofile_permanent_notes.py
Normal file
18
bookmarks/migrations/0023_userprofile_permanent_notes.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.9 on 2023-05-20 08:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bookmarks', '0022_bookmark_notes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='permanent_notes',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -50,6 +50,7 @@ class Bookmark(models.Model):
|
|||||||
url = models.CharField(max_length=2048, validators=[BookmarkURLValidator()])
|
url = models.CharField(max_length=2048, validators=[BookmarkURLValidator()])
|
||||||
title = models.CharField(max_length=512, blank=True)
|
title = models.CharField(max_length=512, blank=True)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
|
notes = models.TextField(blank=True)
|
||||||
website_title = models.CharField(max_length=512, blank=True, null=True)
|
website_title = models.CharField(max_length=512, blank=True, null=True)
|
||||||
website_description = models.TextField(blank=True, null=True)
|
website_description = models.TextField(blank=True, null=True)
|
||||||
web_archive_snapshot_url = models.CharField(max_length=2048, blank=True)
|
web_archive_snapshot_url = models.CharField(max_length=2048, blank=True)
|
||||||
@@ -110,6 +111,7 @@ class BookmarkForm(forms.ModelForm):
|
|||||||
'tag_string',
|
'tag_string',
|
||||||
'title',
|
'title',
|
||||||
'description',
|
'description',
|
||||||
|
'notes',
|
||||||
'website_title',
|
'website_title',
|
||||||
'website_description',
|
'website_description',
|
||||||
'unread',
|
'unread',
|
||||||
@@ -117,6 +119,10 @@ class BookmarkForm(forms.ModelForm):
|
|||||||
'auto_close',
|
'auto_close',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_notes(self):
|
||||||
|
return self.instance and self.instance.notes
|
||||||
|
|
||||||
|
|
||||||
class BookmarkFilters:
|
class BookmarkFilters:
|
||||||
def __init__(self, request: WSGIRequest):
|
def __init__(self, request: WSGIRequest):
|
||||||
@@ -153,6 +159,12 @@ class UserProfile(models.Model):
|
|||||||
(WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'),
|
(WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'),
|
||||||
(WEB_ARCHIVE_INTEGRATION_ENABLED, 'Enabled'),
|
(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)
|
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)
|
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,
|
bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, blank=False,
|
||||||
@@ -161,14 +173,19 @@ class UserProfile(models.Model):
|
|||||||
default=BOOKMARK_LINK_TARGET_BLANK)
|
default=BOOKMARK_LINK_TARGET_BLANK)
|
||||||
web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False,
|
web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False,
|
||||||
default=WEB_ARCHIVE_INTEGRATION_DISABLED)
|
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_sharing = models.BooleanField(default=False, null=False)
|
||||||
enable_favicons = models.BooleanField(default=False, null=False)
|
enable_favicons = models.BooleanField(default=False, null=False)
|
||||||
|
display_url = models.BooleanField(default=False, null=False)
|
||||||
|
permanent_notes = models.BooleanField(default=False, null=False)
|
||||||
|
|
||||||
|
|
||||||
class UserProfileForm(forms.ModelForm):
|
class UserProfileForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserProfile
|
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', 'permanent_notes']
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=get_user_model())
|
@receiver(post_save, sender=get_user_model())
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
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
|
from bookmarks.utils import unique
|
||||||
|
|
||||||
|
|
||||||
def query_bookmarks(user: User, query_string: str) -> QuerySet:
|
def query_bookmarks(user: User, profile: UserProfile, query_string: str) -> QuerySet:
|
||||||
return _base_bookmarks_query(user, query_string) \
|
return _base_bookmarks_query(user, profile, query_string) \
|
||||||
.filter(is_archived=False)
|
.filter(is_archived=False)
|
||||||
|
|
||||||
|
|
||||||
def query_archived_bookmarks(user: User, query_string: str) -> QuerySet:
|
def query_archived_bookmarks(user: User, profile: UserProfile, query_string: str) -> QuerySet:
|
||||||
return _base_bookmarks_query(user, query_string) \
|
return _base_bookmarks_query(user, profile, query_string) \
|
||||||
.filter(is_archived=True)
|
.filter(is_archived=True)
|
||||||
|
|
||||||
|
|
||||||
def query_shared_bookmarks(user: Optional[User], query_string: str) -> QuerySet:
|
def query_shared_bookmarks(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet:
|
||||||
return _base_bookmarks_query(user, query_string) \
|
return _base_bookmarks_query(user, profile, query_string) \
|
||||||
.filter(shared=True) \
|
.filter(shared=True) \
|
||||||
.filter(owner__profile__enable_sharing=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
|
query_set = Bookmark.objects
|
||||||
|
|
||||||
# Filter for user
|
# Filter for user
|
||||||
@@ -35,13 +35,17 @@ def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet:
|
|||||||
|
|
||||||
# Filter for search terms and tags
|
# Filter for search terms and tags
|
||||||
for term in query['search_terms']:
|
for term in query['search_terms']:
|
||||||
query_set = query_set.filter(
|
conditions = Q(title__icontains=term) \
|
||||||
Q(title__contains=term)
|
| Q(description__icontains=term) \
|
||||||
| Q(description__contains=term)
|
| Q(notes__icontains=term) \
|
||||||
| Q(website_title__contains=term)
|
| Q(website_title__icontains=term) \
|
||||||
| Q(website_description__contains=term)
|
| Q(website_description__icontains=term) \
|
||||||
| Q(url__contains=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']:
|
for tag_name in query['tag_names']:
|
||||||
query_set = query_set.filter(
|
query_set = query_set.filter(
|
||||||
@@ -65,32 +69,32 @@ def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet:
|
|||||||
return query_set
|
return query_set
|
||||||
|
|
||||||
|
|
||||||
def query_bookmark_tags(user: User, query_string: str) -> QuerySet:
|
def query_bookmark_tags(user: User, profile: UserProfile, query_string: str) -> QuerySet:
|
||||||
bookmarks_query = query_bookmarks(user, query_string)
|
bookmarks_query = query_bookmarks(user, profile, query_string)
|
||||||
|
|
||||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||||
|
|
||||||
return query_set.distinct()
|
return query_set.distinct()
|
||||||
|
|
||||||
|
|
||||||
def query_archived_bookmark_tags(user: User, query_string: str) -> QuerySet:
|
def query_archived_bookmark_tags(user: User, profile: UserProfile, query_string: str) -> QuerySet:
|
||||||
bookmarks_query = query_archived_bookmarks(user, query_string)
|
bookmarks_query = query_archived_bookmarks(user, profile, query_string)
|
||||||
|
|
||||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||||
|
|
||||||
return query_set.distinct()
|
return query_set.distinct()
|
||||||
|
|
||||||
|
|
||||||
def query_shared_bookmark_tags(user: Optional[User], query_string: str) -> QuerySet:
|
def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet:
|
||||||
bookmarks_query = query_shared_bookmarks(user, query_string)
|
bookmarks_query = query_shared_bookmarks(user, profile, query_string)
|
||||||
|
|
||||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||||
|
|
||||||
return query_set.distinct()
|
return query_set.distinct()
|
||||||
|
|
||||||
|
|
||||||
def query_shared_bookmark_users(query_string: str) -> QuerySet:
|
def query_shared_bookmark_users(profile: UserProfile, query_string: str) -> QuerySet:
|
||||||
bookmarks_query = query_shared_bookmarks(None, query_string)
|
bookmarks_query = query_shared_bookmarks(None, profile, query_string)
|
||||||
|
|
||||||
query_set = User.objects.filter(bookmark__in=bookmarks_query)
|
query_set = User.objects.filter(bookmark__in=bookmarks_query)
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ def untag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_us
|
|||||||
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
|
def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
|
||||||
to_bookmark.title = from_bookmark.title
|
to_bookmark.title = from_bookmark.title
|
||||||
to_bookmark.description = from_bookmark.description
|
to_bookmark.description = from_bookmark.description
|
||||||
|
to_bookmark.notes = from_bookmark.notes
|
||||||
to_bookmark.unread = from_bookmark.unread
|
to_bookmark.unread = from_bookmark.unread
|
||||||
to_bookmark.shared = from_bookmark.shared
|
to_bookmark.shared = from_bookmark.shared
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
(function () {
|
(function () {
|
||||||
|
function allowBulkEdit() {
|
||||||
|
return !!document.getElementById('bulk-edit-mode');
|
||||||
|
}
|
||||||
|
|
||||||
function setupBulkEdit() {
|
function setupBulkEdit() {
|
||||||
|
if (!allowBulkEdit()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bulkEditToggle = document.getElementById('bulk-edit-mode')
|
const bulkEditToggle = document.getElementById('bulk-edit-mode')
|
||||||
const bulkEditBar = document.querySelector('.bulk-edit-bar')
|
const bulkEditBar = document.querySelector('.bulk-edit-bar')
|
||||||
const singleToggles = document.querySelectorAll('.bulk-edit-toggle input')
|
const singleToggles = document.querySelectorAll('.bulk-edit-toggle input')
|
||||||
@@ -64,6 +72,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupBulkEditTagAutoComplete() {
|
function setupBulkEditTagAutoComplete() {
|
||||||
|
if (!allowBulkEdit()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
const tagInput = document.getElementById('bulk-edit-tags-input');
|
const tagInput = document.getElementById('bulk-edit-tags-input');
|
||||||
const apiBaseUrl = document.documentElement.dataset.apiBaseUrl || '';
|
const apiBaseUrl = document.documentElement.dataset.apiBaseUrl || '';
|
||||||
@@ -121,7 +133,39 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupNotes() {
|
||||||
|
// Shortcut for toggling all notes
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
// Filter for shortcut key
|
||||||
|
if (event.key !== 'e') return;
|
||||||
|
// Skip if event occurred within an input element
|
||||||
|
const targetNodeName = event.target.nodeName;
|
||||||
|
const isInputTarget = targetNodeName === 'INPUT'
|
||||||
|
|| targetNodeName === 'SELECT'
|
||||||
|
|| targetNodeName === 'TEXTAREA';
|
||||||
|
|
||||||
|
if (isInputTarget) return;
|
||||||
|
|
||||||
|
const list = document.querySelector('.bookmark-list');
|
||||||
|
list.classList.toggle('show-notes');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle notes for single bookmark
|
||||||
|
const bookmarks = document.querySelectorAll('.bookmark-list li');
|
||||||
|
bookmarks.forEach(bookmark => {
|
||||||
|
const toggleButton = bookmark.querySelector('.toggle-notes');
|
||||||
|
if (toggleButton) {
|
||||||
|
toggleButton.addEventListener('click', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
bookmark.classList.toggle('show-notes');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setupBulkEdit();
|
setupBulkEdit();
|
||||||
setupBulkEditTagAutoComplete();
|
setupBulkEditTagAutoComplete();
|
||||||
setupListNavigation();
|
setupListNavigation();
|
||||||
|
setupNotes();
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ a:visited:hover {
|
|||||||
color: $link-color-dark;
|
color: $link-color-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: $gray-color-dark;
|
||||||
|
background-color: $code-bg-color;
|
||||||
|
box-shadow: 1px 1px 0 $code-shadow-color;
|
||||||
|
}
|
||||||
|
|
||||||
// Increase spacing between columns
|
// Increase spacing between columns
|
||||||
.container > .columns > .column:not(:first-child) {
|
.container > .columns > .column:not(:first-child) {
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
@@ -102,3 +108,12 @@ a:visited:hover {
|
|||||||
font-weight: bold;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,11 +43,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bookmark list */
|
||||||
ul.bookmark-list {
|
ul.bookmark-list {
|
||||||
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bookmarks */
|
||||||
|
ul.bookmark-list li {
|
||||||
|
|
||||||
|
.bulk-edit-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.title a {
|
.title a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -64,6 +72,10 @@ ul.bookmark-list {
|
|||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.url-display {
|
||||||
|
color: $secondary-link-color;
|
||||||
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
color: $gray-color-dark;
|
color: $gray-color-dark;
|
||||||
|
|
||||||
@@ -72,31 +84,44 @@ ul.bookmark-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions > *:not(:last-child) {
|
.actions {
|
||||||
margin-right: 0.1rem;
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions .date-label a {
|
.actions {
|
||||||
color: $gray-color;
|
> *:not(:last-child) {
|
||||||
}
|
margin-right: 0.4rem;
|
||||||
|
|
||||||
.actions .btn-link {
|
|
||||||
color: $gray-color;
|
|
||||||
padding: 0;
|
|
||||||
height: auto;
|
|
||||||
vertical-align: unset;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:hover,
|
|
||||||
&:active,
|
|
||||||
&.active {
|
|
||||||
color: $gray-color-dark;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.bulk-edit-toggle {
|
a, button {
|
||||||
display: none;
|
color: $gray-color;
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
vertical-align: unset;
|
||||||
|
border: none;
|
||||||
|
transition: none;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&.active {
|
||||||
|
color: $gray-color-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-notes {
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +201,68 @@ ul.bookmark-list {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details.notes textarea {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bookmark notes */
|
||||||
|
ul.bookmark-list {
|
||||||
|
.notes {
|
||||||
|
display: none;
|
||||||
|
max-height: 300px;
|
||||||
|
margin: 4px 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.show-notes .notes,
|
||||||
|
li.show-notes .notes {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bookmark notes markdown styles */
|
||||||
|
ul.bookmark-list .notes-content {
|
||||||
|
& {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, ul, ol, pre, blockquote {
|
||||||
|
margin: 0 0 0.4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
> *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin-left: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li, ol li {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
background-color: $code-bg-color;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> pre:first-child:last-child {
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bookmark actions / bulk edit */
|
/* Bookmark actions / bulk edit */
|
||||||
|
|||||||
@@ -21,8 +21,13 @@ $link-color: $primary-color !default;
|
|||||||
$link-color-dark: darken($link-color, 5%) !default;
|
$link-color-dark: darken($link-color, 5%) !default;
|
||||||
$link-color-light: $link-color !default;
|
$link-color-light: $link-color !default;
|
||||||
|
|
||||||
|
$secondary-link-color: rgba(168, 177, 255, 0.73);
|
||||||
|
|
||||||
$alternative-color: #59bdb9;
|
$alternative-color: #59bdb9;
|
||||||
$alternative-color-dark: #73f1eb;
|
$alternative-color-dark: #73f1eb;
|
||||||
|
|
||||||
|
$code-bg-color: rgba(255, 255, 255, 0.1);
|
||||||
|
$code-shadow-color: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
/* Dark theme specific */
|
/* Dark theme specific */
|
||||||
$dt-primary-button-color: #5761cb !default;
|
$dt-primary-button-color: #5761cb !default;
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ $html-font-size: 18px !default;
|
|||||||
|
|
||||||
$alternative-color: #05a6a3;
|
$alternative-color: #05a6a3;
|
||||||
$alternative-color-dark: darken($alternative-color, 5%);
|
$alternative-color-dark: darken($alternative-color, 5%);
|
||||||
|
|
||||||
|
$secondary-link-color: rgba(87, 85, 217, 0.64);
|
||||||
|
|
||||||
|
$code-bg-color: rgba(0, 0, 0, 0.05);
|
||||||
|
$code-shadow-color: rgba(0, 0, 0, 0.15);
|
||||||
|
|||||||
@@ -1,102 +1,128 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load shared %}
|
{% load shared %}
|
||||||
{% load pagination %}
|
{% load pagination %}
|
||||||
{% htmlmin %}
|
<ul class="bookmark-list{% if request.user.profile.permanent_notes %} show-notes{% endif %}">
|
||||||
<ul class="bookmark-list">
|
{% for bookmark in bookmarks %}
|
||||||
{% for bookmark in bookmarks %}
|
<li data-is-bookmark-item>
|
||||||
<li data-is-bookmark-item>
|
<label class="form-checkbox bulk-edit-toggle">
|
||||||
<label class="form-checkbox bulk-edit-toggle">
|
<input type="checkbox" name="bookmark_id" value="{{ bookmark.id }}">
|
||||||
<input type="checkbox" name="bookmark_id" value="{{ bookmark.id }}">
|
<i class="form-icon"></i>
|
||||||
<i class="form-icon"></i>
|
</label>
|
||||||
</label>
|
<div class="title">
|
||||||
<div class="title">
|
<a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
|
||||||
|
class="{% if bookmark.unread %}text-italic{% endif %}">
|
||||||
|
{% if bookmark.favicon_file and request.user.profile.enable_favicons %}
|
||||||
|
<img src="{% static bookmark.favicon_file %}" alt="">
|
||||||
|
{% endif %}
|
||||||
|
{{ 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"
|
<a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
|
||||||
class="{% if bookmark.unread %}text-italic{% endif %}">
|
class="url-display text-sm">
|
||||||
{% if bookmark.favicon_file and request.user.profile.enable_favicons %}
|
{{ bookmark.url }}
|
||||||
<img src="{% static bookmark.favicon_file %}" alt="">
|
|
||||||
{% endif %}
|
|
||||||
{{ bookmark.resolved_title }}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="description truncate">
|
{% endif %}
|
||||||
{% if bookmark.tag_names %}
|
<div class="description truncate">
|
||||||
<span>
|
{% if bookmark.tag_names %}
|
||||||
|
<span>
|
||||||
{% for tag_name in bookmark.tag_names %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if bookmark.tag_names and bookmark.resolved_description %} | {% endif %}
|
{% if bookmark.tag_names and bookmark.resolved_description %} | {% endif %}
|
||||||
{% if bookmark.resolved_description %}
|
{% if bookmark.resolved_description %}
|
||||||
<span>{{ bookmark.resolved_description }}</span>
|
<span>{{ bookmark.resolved_description }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if bookmark.notes %}
|
||||||
|
<div class="notes bg-gray text-gray-dark">
|
||||||
|
<div class="notes-content">
|
||||||
|
{% markdown bookmark.notes %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
{% endif %}
|
||||||
{% if request.user.profile.bookmark_date_display == 'relative' %}
|
<div class="actions text-gray text-sm">
|
||||||
<span class="date-label text-gray text-sm">
|
{% if request.user.profile.bookmark_date_display == 'relative' %}
|
||||||
|
<span>
|
||||||
{% if bookmark.web_archive_snapshot_url %}
|
{% if bookmark.web_archive_snapshot_url %}
|
||||||
<a href="{{ bookmark.web_archive_snapshot_url }}"
|
<a href="{{ bookmark.web_archive_snapshot_url }}"
|
||||||
title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}"
|
title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}"
|
||||||
rel="noopener">
|
rel="noopener">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span>{{ bookmark.date_added|humanize_relative_date }}</span>
|
<span>{{ bookmark.date_added|humanize_relative_date }}</span>
|
||||||
{% if bookmark.web_archive_snapshot_url %}
|
{% if bookmark.web_archive_snapshot_url %}
|
||||||
<span>∞</span>
|
∞
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
<span class="text-gray text-sm">|</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.user.profile.bookmark_date_display == 'absolute' %}
|
|
||||||
<span class="date-label text-gray text-sm">
|
|
||||||
{% if bookmark.web_archive_snapshot_url %}
|
|
||||||
<a href="{{ bookmark.web_archive_snapshot_url }}"
|
|
||||||
title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}"
|
|
||||||
rel="noopener">
|
|
||||||
{% endif %}
|
|
||||||
<span>{{ bookmark.date_added|humanize_absolute_date }}</span>
|
|
||||||
{% if bookmark.web_archive_snapshot_url %}
|
|
||||||
<span>∞</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
<span class="text-gray text-sm">|</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if bookmark.owner == request.user %}
|
|
||||||
{# Bookmark owner actions #}
|
|
||||||
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}"
|
|
||||||
class="btn btn-link btn-sm">Edit</a>
|
|
||||||
{% if bookmark.is_archived %}
|
|
||||||
<button type="submit" name="unarchive" value="{{ bookmark.id }}"
|
|
||||||
class="btn btn-link btn-sm">Unarchive
|
|
||||||
</button>
|
|
||||||
{% else %}
|
|
||||||
<button type="submit" name="archive" value="{{ bookmark.id }}"
|
|
||||||
class="btn btn-link btn-sm">Archive
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="submit" name="remove" value="{{ bookmark.id }}"
|
</span>
|
||||||
class="btn btn-link btn-sm btn-confirmation">Remove
|
<span class="separator">|</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.user.profile.bookmark_date_display == 'absolute' %}
|
||||||
|
<span>
|
||||||
|
{% if bookmark.web_archive_snapshot_url %}
|
||||||
|
<a href="{{ bookmark.web_archive_snapshot_url }}"
|
||||||
|
title="Show snapshot on the Internet Archive Wayback Machine" target="{{ link_target }}"
|
||||||
|
rel="noopener">
|
||||||
|
{% endif %}
|
||||||
|
<span>{{ bookmark.date_added|humanize_absolute_date }}</span>
|
||||||
|
{% if bookmark.web_archive_snapshot_url %}
|
||||||
|
∞
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="separator">|</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if bookmark.owner == request.user %}
|
||||||
|
{# Bookmark owner actions #}
|
||||||
|
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}">Edit</a>
|
||||||
|
{% if bookmark.is_archived %}
|
||||||
|
<button type="submit" name="unarchive" value="{{ bookmark.id }}"
|
||||||
|
class="btn btn-link btn-sm">Unarchive
|
||||||
</button>
|
</button>
|
||||||
{% if bookmark.unread %}
|
|
||||||
<span class="text-gray text-sm">|</span>
|
|
||||||
<button type="submit" name="mark_as_read" value="{{ bookmark.id }}"
|
|
||||||
class="btn btn-link btn-sm">Mark as read
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{# Shared bookmark actions #}
|
<button type="submit" name="archive" value="{{ bookmark.id }}"
|
||||||
<span class="text-gray text-sm">Shared by
|
class="btn btn-link btn-sm">Archive
|
||||||
<a class="text-gray"
|
</button>
|
||||||
href="?{% replace_query_param user=bookmark.owner.username %}">{{ bookmark.owner.username }}</a>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
<button type="submit" name="remove" value="{{ bookmark.id }}"
|
||||||
</li>
|
class="btn btn-link btn-sm btn-confirmation">Remove
|
||||||
{% endfor %}
|
</button>
|
||||||
</ul>
|
{% if bookmark.unread %}
|
||||||
|
<span class="separator">|</span>
|
||||||
|
<button type="submit" name="mark_as_read" value="{{ bookmark.id }}"
|
||||||
|
class="btn btn-link btn-sm">Mark as read
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{# Shared bookmark actions #}
|
||||||
|
<span>Shared by
|
||||||
|
<a href="?{% replace_query_param user=bookmark.owner.username %}">{{ bookmark.owner.username }}</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if bookmark.notes and not request.user.profile.permanent_notes %}
|
||||||
|
<span class="separator">|</span>
|
||||||
|
<button class="btn btn-link btn-sm toggle-notes" title="Toggle notes">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-notes" width="16" height="16"
|
||||||
|
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path>
|
||||||
|
<path d="M9 7l6 0"></path>
|
||||||
|
<path d="M9 11l6 0"></path>
|
||||||
|
<path d="M9 15l4 0"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Notes</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="bookmark-pagination">
|
<div class="bookmark-pagination">
|
||||||
{% pagination bookmarks %}
|
{% pagination bookmarks %}
|
||||||
</div>
|
</div>
|
||||||
{% endhtmlmin %}
|
|
||||||
|
|||||||
@@ -67,6 +67,19 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ form.description.errors }}
|
{{ form.description.errors }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<details class="notes"{% if form.has_notes %} open{% endif %}>
|
||||||
|
<summary>
|
||||||
|
<span class="form-label d-inline-block">Notes</span>
|
||||||
|
</summary>
|
||||||
|
<label for="{{ form.notes.id_for_label }}" class="text-assistive">Notes</label>
|
||||||
|
{{ form.notes|add_class:"form-input"|attr:"rows:8" }}
|
||||||
|
<div class="form-input-hint">
|
||||||
|
Additional notes, supports Markdown.
|
||||||
|
</div>
|
||||||
|
{{ form.notes.errors }}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.unread.id_for_label }}" class="form-checkbox">
|
<label for="{{ form.unread.id_for_label }}" class="form-checkbox">
|
||||||
{{ form.unread }}
|
{{ form.unread }}
|
||||||
@@ -128,6 +141,8 @@
|
|||||||
const urlInput = document.getElementById('{{ form.url.id_for_label }}');
|
const urlInput = document.getElementById('{{ form.url.id_for_label }}');
|
||||||
const titleInput = document.getElementById('{{ form.title.id_for_label }}');
|
const titleInput = document.getElementById('{{ form.title.id_for_label }}');
|
||||||
const descriptionInput = document.getElementById('{{ form.description.id_for_label }}');
|
const descriptionInput = document.getElementById('{{ form.description.id_for_label }}');
|
||||||
|
const notesDetails = document.querySelector('form details.notes');
|
||||||
|
const notesInput = document.getElementById('{{ form.notes.id_for_label }}');
|
||||||
const tagsInput = document.getElementById('{{ form.tag_string.id_for_label }}');
|
const tagsInput = document.getElementById('{{ form.tag_string.id_for_label }}');
|
||||||
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
|
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
|
||||||
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
||||||
@@ -149,11 +164,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateInput(input, value) {
|
function updateInput(input, value) {
|
||||||
input.value = value;
|
if (!input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCheckbox(input, value) {
|
function updateCheckbox(input, value) {
|
||||||
input.checked = value;
|
if (!input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.checked = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkUrl() {
|
function checkUrl() {
|
||||||
@@ -179,8 +200,10 @@
|
|||||||
|
|
||||||
if (existingBookmark && !editedBookmarkId) {
|
if (existingBookmark && !editedBookmarkId) {
|
||||||
bookmarkExistsHint.style['display'] = 'block';
|
bookmarkExistsHint.style['display'] = 'block';
|
||||||
|
notesDetails.open = !!existingBookmark.notes;
|
||||||
updateInput(titleInput, existingBookmark.title);
|
updateInput(titleInput, existingBookmark.title);
|
||||||
updateInput(descriptionInput, existingBookmark.description);
|
updateInput(descriptionInput, existingBookmark.description);
|
||||||
|
updateInput(notesInput, existingBookmark.notes);
|
||||||
updateInput(tagsInput, existingBookmark.tag_names.join(" "));
|
updateInput(tagsInput, existingBookmark.tag_names.join(" "));
|
||||||
updateCheckbox(unreadCheckbox, existingBookmark.unread);
|
updateCheckbox(unreadCheckbox, existingBookmark.unread);
|
||||||
updateCheckbox(sharedCheckbox, existingBookmark.shared);
|
updateCheckbox(sharedCheckbox, existingBookmark.shared);
|
||||||
@@ -201,6 +224,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupEditAutoValueButton(titleInput);
|
||||||
|
setupEditAutoValueButton(descriptionInput);
|
||||||
|
|
||||||
// Fetch initial website data if we have a URL, and we are not editing an existing bookmark
|
// Fetch initial website data if we have a URL, and we are not editing an existing bookmark
|
||||||
// For existing bookmarks we get the website metadata through hidden inputs
|
// For existing bookmarks we get the website metadata through hidden inputs
|
||||||
if (urlInput.value && !editedBookmarkId) {
|
if (urlInput.value && !editedBookmarkId) {
|
||||||
@@ -213,9 +239,6 @@
|
|||||||
updatePlaceholder(titleInput, websiteTitleInput.value);
|
updatePlaceholder(titleInput, websiteTitleInput.value);
|
||||||
updatePlaceholder(descriptionInput, websiteDescriptionInput.value);
|
updatePlaceholder(descriptionInput, websiteDescriptionInput.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEditAutoValueButton(titleInput);
|
|
||||||
setupEditAutoValueButton(descriptionInput);
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,4 +45,5 @@
|
|||||||
|
|
||||||
<script src="{% static "bundle.js" %}"></script>
|
<script src="{% static "bundle.js" %}"></script>
|
||||||
<script src="{% static "shared.js" %}"></script>
|
<script src="{% static "shared.js" %}"></script>
|
||||||
|
<script src="{% static "bookmark_list.js" %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% if has_selected_tags %}
|
{% if has_selected_tags %}
|
||||||
<p class="selected-tags">
|
<p class="selected-tags">
|
||||||
{% for tag in 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">
|
class="text-bold mr-2">
|
||||||
<span>-{{ tag.name }}</span>
|
<span>-{{ tag.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -17,14 +17,14 @@
|
|||||||
{% for tag in group.tags %}
|
{% for tag in group.tags %}
|
||||||
{# Highlight first char of first tag in group #}
|
{# Highlight first char of first tag in group #}
|
||||||
{% if forloop.counter == 1 %}
|
{% 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>
|
class="mr-2" data-is-tag-item>
|
||||||
<span
|
<span
|
||||||
class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
|
class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{# Render remaining tags normally #}
|
{# 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>
|
class="mr-2" data-is-tag-item>
|
||||||
<span>{{ tag.name }}</span>
|
<span>{{ tag.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -29,6 +29,25 @@
|
|||||||
be hidden.
|
be hidden.
|
||||||
</div>
|
</div>
|
||||||
</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.permanent_notes.id_for_label }}" class="form-checkbox">
|
||||||
|
{{ form.permanent_notes }}
|
||||||
|
<i class="form-icon"></i> Show notes permanently
|
||||||
|
</label>
|
||||||
|
<div class="form-input-hint">
|
||||||
|
Whether to show bookmark notes permanently, without having to toggle them individually.
|
||||||
|
Alternatively the keyboard shortcut <code>e</code> can be used to temporarily show all notes.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.bookmark_link_target.id_for_label }}" class="form-label">Open bookmarks in</label>
|
<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" }}
|
{{ form.bookmark_link_target|add_class:"form-select col-2 col-sm-12" }}
|
||||||
@@ -36,6 +55,15 @@
|
|||||||
Whether to open bookmarks a new page or in the same page.
|
Whether to open bookmarks a new page or in the same page.
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group">
|
||||||
<label for="{{ form.enable_favicons.id_for_label }}" class="form-checkbox">
|
<label for="{{ form.enable_favicons.id_for_label }}" class="form-checkbox">
|
||||||
{{ form.enable_favicons }}
|
{{ form.enable_favicons }}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
import bleach
|
||||||
|
import markdown
|
||||||
|
from bleach_allowlist import markdown_tags, markdown_attrs
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from bookmarks import utils
|
from bookmarks import utils
|
||||||
|
from bookmarks.models import UserProfile
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@@ -19,36 +24,39 @@ def update_query_string(context, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def append_to_query_param(context, **kwargs):
|
def add_tag_to_query(context, tag_name: str):
|
||||||
query = context.request.GET.copy()
|
params = context.request.GET.copy()
|
||||||
|
|
||||||
# Append to or create query param
|
# Append to or create query string
|
||||||
for key in kwargs:
|
if params.__contains__('q'):
|
||||||
if query.__contains__(key):
|
query_string = params.__getitem__('q') + ' '
|
||||||
value = query.__getitem__(key) + ' '
|
else:
|
||||||
else:
|
query_string = ''
|
||||||
value = ''
|
query_string = query_string + '#' + tag_name
|
||||||
value = value + kwargs[key]
|
params.__setitem__('q', query_string)
|
||||||
query.__setitem__(key, value)
|
|
||||||
|
|
||||||
return query.urlencode()
|
return params.urlencode()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def remove_from_query_param(context, **kwargs):
|
def remove_tag_from_query(context, tag_name: str):
|
||||||
query = context.request.GET.copy()
|
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
|
return params.urlencode()
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
@@ -109,3 +117,19 @@ class HtmlMinNode(template.Node):
|
|||||||
output = re.sub(r'\s+', ' ', output)
|
output = re.sub(r'\s+', ' ', output)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(name="markdown", takes_context=True)
|
||||||
|
def render_markdown(context, markdown_text):
|
||||||
|
# naive approach to reusing the renderer for a single request
|
||||||
|
# works for bookmark list for now
|
||||||
|
if not ('markdown_renderer' in context):
|
||||||
|
renderer = markdown.Markdown(extensions=['fenced_code', 'nl2br'])
|
||||||
|
context['markdown_renderer'] = renderer
|
||||||
|
else:
|
||||||
|
renderer = context['markdown_renderer']
|
||||||
|
|
||||||
|
as_html = renderer.convert(markdown_text)
|
||||||
|
sanitized_html = bleach.clean(as_html, markdown_tags, markdown_attrs)
|
||||||
|
|
||||||
|
return mark_safe(sanitized_html)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class BookmarkFactoryMixin:
|
|||||||
url: str = '',
|
url: str = '',
|
||||||
title: str = '',
|
title: str = '',
|
||||||
description: str = '',
|
description: str = '',
|
||||||
|
notes: str = '',
|
||||||
website_title: str = '',
|
website_title: str = '',
|
||||||
website_description: str = '',
|
website_description: str = '',
|
||||||
web_archive_snapshot_url: str = '',
|
web_archive_snapshot_url: str = '',
|
||||||
@@ -48,6 +49,7 @@ class BookmarkFactoryMixin:
|
|||||||
url=url,
|
url=url,
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
|
notes=notes,
|
||||||
website_title=website_title,
|
website_title=website_title,
|
||||||
website_description=website_description,
|
website_description=website_description,
|
||||||
date_added=timezone.now(),
|
date_added=timezone.now(),
|
||||||
|
|||||||
@@ -158,6 +158,37 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
|||||||
|
|
||||||
self.assertSelectedTags(response, [tags[0], tags[1]])
|
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):
|
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||||
visible_bookmarks = [
|
visible_bookmarks = [
|
||||||
self.setup_bookmark(is_archived=True),
|
self.setup_bookmark(is_archived=True),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
'tag_string': 'editedtag1 editedtag2',
|
'tag_string': 'editedtag1 editedtag2',
|
||||||
'title': 'edited title',
|
'title': 'edited title',
|
||||||
'description': 'edited description',
|
'description': 'edited description',
|
||||||
|
'notes': 'edited notes',
|
||||||
'unread': False,
|
'unread': False,
|
||||||
'shared': False,
|
'shared': False,
|
||||||
}
|
}
|
||||||
@@ -37,6 +38,7 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(bookmark.url, form_data['url'])
|
self.assertEqual(bookmark.url, form_data['url'])
|
||||||
self.assertEqual(bookmark.title, form_data['title'])
|
self.assertEqual(bookmark.title, form_data['title'])
|
||||||
self.assertEqual(bookmark.description, form_data['description'])
|
self.assertEqual(bookmark.description, form_data['description'])
|
||||||
|
self.assertEqual(bookmark.notes, form_data['notes'])
|
||||||
self.assertEqual(bookmark.unread, form_data['unread'])
|
self.assertEqual(bookmark.unread, form_data['unread'])
|
||||||
self.assertEqual(bookmark.shared, form_data['shared'])
|
self.assertEqual(bookmark.shared, form_data['shared'])
|
||||||
self.assertEqual(bookmark.tags.count(), 2)
|
self.assertEqual(bookmark.tags.count(), 2)
|
||||||
@@ -74,7 +76,8 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
bookmark = self.setup_bookmark(tags=[tag1, tag2], title='edited title', description='edited description',
|
bookmark = self.setup_bookmark(tags=[tag1, tag2], title='edited title', description='edited description',
|
||||||
website_title='website title', website_description='website description')
|
notes='edited notes', website_title='website title',
|
||||||
|
website_description='website description')
|
||||||
|
|
||||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
@@ -101,6 +104,12 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
</textarea>
|
</textarea>
|
||||||
''', html)
|
''', html)
|
||||||
|
|
||||||
|
self.assertInHTML(f'''
|
||||||
|
<textarea name="notes" cols="40" rows="8" class="form-input" id="id_notes">
|
||||||
|
{bookmark.notes}
|
||||||
|
</textarea>
|
||||||
|
''', html)
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
<input type="hidden" name="website_title" id="id_website_title"
|
<input type="hidden" name="website_title" id="id_website_title"
|
||||||
value="{bookmark.website_title}">
|
value="{bookmark.website_title}">
|
||||||
@@ -184,3 +193,15 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
<span>Share</span>
|
<span>Share</span>
|
||||||
</label>
|
</label>
|
||||||
''', html, count=1)
|
''', html, count=1)
|
||||||
|
|
||||||
|
def test_should_hide_notes_if_there_are_no_notes(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||||
|
|
||||||
|
self.assertContains(response, '<details class="notes">', count=1)
|
||||||
|
|
||||||
|
def test_should_show_notes_if_there_are_notes(self):
|
||||||
|
bookmark = self.setup_bookmark(notes='test notes')
|
||||||
|
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||||
|
|
||||||
|
self.assertContains(response, '<details class="notes" open>', count=1)
|
||||||
|
|||||||
@@ -155,7 +155,38 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
]
|
]
|
||||||
self.setup_bookmark(tags=tags)
|
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]])
|
self.assertSelectedTags(response, [tags[0], tags[1]])
|
||||||
|
|
||||||
@@ -196,8 +227,7 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
return_url = urllib.parse.quote_plus(url)
|
return_url = urllib.parse.quote_plus(url)
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
<a href="{edit_url}?return_url={return_url}"
|
<a href="{edit_url}?return_url={return_url}">Edit</a>
|
||||||
class="btn btn-link btn-sm">Edit</a>
|
|
||||||
''', html)
|
''', html)
|
||||||
|
|
||||||
# with query params
|
# with query params
|
||||||
@@ -208,6 +238,5 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
return_url = urllib.parse.quote_plus(url)
|
return_url = urllib.parse.quote_plus(url)
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
<a href="{edit_url}?return_url={return_url}"
|
<a href="{edit_url}?return_url={return_url}">Edit</a>
|
||||||
class="btn btn-link btn-sm">Edit</a>
|
|
||||||
''', html)
|
''', html)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
'tag_string': 'tag1 tag2',
|
'tag_string': 'tag1 tag2',
|
||||||
'title': 'test title',
|
'title': 'test title',
|
||||||
'description': 'test description',
|
'description': 'test description',
|
||||||
|
'notes': 'test notes',
|
||||||
'unread': False,
|
'unread': False,
|
||||||
'shared': False,
|
'shared': False,
|
||||||
'auto_close': '',
|
'auto_close': '',
|
||||||
@@ -37,6 +38,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(bookmark.url, form_data['url'])
|
self.assertEqual(bookmark.url, form_data['url'])
|
||||||
self.assertEqual(bookmark.title, form_data['title'])
|
self.assertEqual(bookmark.title, form_data['title'])
|
||||||
self.assertEqual(bookmark.description, form_data['description'])
|
self.assertEqual(bookmark.description, form_data['description'])
|
||||||
|
self.assertEqual(bookmark.notes, form_data['notes'])
|
||||||
self.assertEqual(bookmark.unread, form_data['unread'])
|
self.assertEqual(bookmark.unread, form_data['unread'])
|
||||||
self.assertEqual(bookmark.shared, form_data['shared'])
|
self.assertEqual(bookmark.shared, form_data['shared'])
|
||||||
self.assertEqual(bookmark.tags.count(), 2)
|
self.assertEqual(bookmark.tags.count(), 2)
|
||||||
@@ -138,3 +140,9 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
<span>Share</span>
|
<span>Share</span>
|
||||||
</label>
|
</label>
|
||||||
''', html, count=1)
|
''', html, count=1)
|
||||||
|
|
||||||
|
def test_should_hide_notes_if_there_are_no_notes(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||||
|
|
||||||
|
self.assertContains(response, '<details class="notes">', count=1)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||||||
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
|
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
|
||||||
self.tag1 = self.setup_tag()
|
self.tag1 = self.setup_tag()
|
||||||
self.tag2 = self.setup_tag()
|
self.tag2 = self.setup_tag()
|
||||||
self.bookmark1 = self.setup_bookmark(tags=[self.tag1, self.tag2])
|
self.bookmark1 = self.setup_bookmark(tags=[self.tag1, self.tag2], notes='Test notes')
|
||||||
self.bookmark2 = self.setup_bookmark()
|
self.bookmark2 = self.setup_bookmark()
|
||||||
self.bookmark3 = self.setup_bookmark(tags=[self.tag2])
|
self.bookmark3 = self.setup_bookmark(tags=[self.tag2])
|
||||||
self.archived_bookmark1 = self.setup_bookmark(is_archived=True, tags=[self.tag1, self.tag2])
|
self.archived_bookmark1 = self.setup_bookmark(is_archived=True, tags=[self.tag1, self.tag2])
|
||||||
@@ -36,6 +36,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||||||
expectation['url'] = bookmark.url
|
expectation['url'] = bookmark.url
|
||||||
expectation['title'] = bookmark.title
|
expectation['title'] = bookmark.title
|
||||||
expectation['description'] = bookmark.description
|
expectation['description'] = bookmark.description
|
||||||
|
expectation['notes'] = bookmark.notes
|
||||||
expectation['website_title'] = bookmark.website_title
|
expectation['website_title'] = bookmark.website_title
|
||||||
expectation['website_description'] = bookmark.website_description
|
expectation['website_description'] = bookmark.website_description
|
||||||
expectation['is_archived'] = bookmark.is_archived
|
expectation['is_archived'] = bookmark.is_archived
|
||||||
@@ -134,6 +135,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||||||
'url': 'https://example.com/',
|
'url': 'https://example.com/',
|
||||||
'title': 'Test title',
|
'title': 'Test title',
|
||||||
'description': 'Test description',
|
'description': 'Test description',
|
||||||
|
'notes': 'Test notes',
|
||||||
'is_archived': False,
|
'is_archived': False,
|
||||||
'unread': False,
|
'unread': False,
|
||||||
'shared': False,
|
'shared': False,
|
||||||
@@ -144,6 +146,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(bookmark.url, data['url'])
|
self.assertEqual(bookmark.url, data['url'])
|
||||||
self.assertEqual(bookmark.title, data['title'])
|
self.assertEqual(bookmark.title, data['title'])
|
||||||
self.assertEqual(bookmark.description, data['description'])
|
self.assertEqual(bookmark.description, data['description'])
|
||||||
|
self.assertEqual(bookmark.notes, data['notes'])
|
||||||
self.assertFalse(bookmark.is_archived, data['is_archived'])
|
self.assertFalse(bookmark.is_archived, data['is_archived'])
|
||||||
self.assertFalse(bookmark.unread, data['unread'])
|
self.assertFalse(bookmark.unread, data['unread'])
|
||||||
self.assertFalse(bookmark.shared, data['shared'])
|
self.assertFalse(bookmark.shared, data['shared'])
|
||||||
@@ -157,6 +160,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||||||
'url': original_bookmark.url,
|
'url': original_bookmark.url,
|
||||||
'title': 'Updated title',
|
'title': 'Updated title',
|
||||||
'description': 'Updated description',
|
'description': 'Updated description',
|
||||||
|
'notes': 'Updated notes',
|
||||||
'unread': True,
|
'unread': True,
|
||||||
'shared': True,
|
'shared': True,
|
||||||
'is_archived': True,
|
'is_archived': True,
|
||||||
@@ -168,6 +172,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(bookmark.url, data['url'])
|
self.assertEqual(bookmark.url, data['url'])
|
||||||
self.assertEqual(bookmark.title, data['title'])
|
self.assertEqual(bookmark.title, data['title'])
|
||||||
self.assertEqual(bookmark.description, data['description'])
|
self.assertEqual(bookmark.description, data['description'])
|
||||||
|
self.assertEqual(bookmark.notes, data['notes'])
|
||||||
# Saving a duplicate bookmark should not modify archive flag - right?
|
# Saving a duplicate bookmark should not modify archive flag - right?
|
||||||
self.assertFalse(bookmark.is_archived)
|
self.assertFalse(bookmark.is_archived)
|
||||||
self.assertEqual(bookmark.unread, data['unread'])
|
self.assertEqual(bookmark.unread, data['unread'])
|
||||||
@@ -265,6 +270,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(updated_bookmark.url, data['url'])
|
self.assertEqual(updated_bookmark.url, data['url'])
|
||||||
self.assertEqual(updated_bookmark.title, '')
|
self.assertEqual(updated_bookmark.title, '')
|
||||||
self.assertEqual(updated_bookmark.description, '')
|
self.assertEqual(updated_bookmark.description, '')
|
||||||
|
self.assertEqual(updated_bookmark.notes, '')
|
||||||
self.assertEqual(updated_bookmark.tag_names, [])
|
self.assertEqual(updated_bookmark.tag_names, [])
|
||||||
|
|
||||||
def test_update_bookmark_unread_flag(self):
|
def test_update_bookmark_unread_flag(self):
|
||||||
@@ -300,6 +306,12 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||||||
self.bookmark1.refresh_from_db()
|
self.bookmark1.refresh_from_db()
|
||||||
self.assertEqual(self.bookmark1.description, data['description'])
|
self.assertEqual(self.bookmark1.description, data['description'])
|
||||||
|
|
||||||
|
data = {'notes': 'Updated notes'}
|
||||||
|
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||||
|
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||||
|
self.bookmark1.refresh_from_db()
|
||||||
|
self.assertEqual(self.bookmark1.notes, data['notes'])
|
||||||
|
|
||||||
data = {'unread': True}
|
data = {'unread': True}
|
||||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -24,22 +24,22 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
|
|
||||||
def assertDateLabel(self, html: str, label_content: str):
|
def assertDateLabel(self, html: str, label_content: str):
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
<span class="date-label text-gray text-sm">
|
<span>
|
||||||
<span>{label_content}</span>
|
<span>{label_content}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray text-sm">|</span>
|
<span class="separator">|</span>
|
||||||
''', html)
|
''', html)
|
||||||
|
|
||||||
def assertWebArchiveLink(self, html: str, label_content: str, url: str, link_target: str = '_blank'):
|
def assertWebArchiveLink(self, html: str, label_content: str, url: str, link_target: str = '_blank'):
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
<span class="date-label text-gray text-sm">
|
<span>
|
||||||
<a href="{url}"
|
<a href="{url}"
|
||||||
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
|
title="Show snapshot on the Internet Archive Wayback Machine" target="{link_target}" rel="noopener">
|
||||||
<span>{label_content}</span>
|
<span>{label_content}</span>
|
||||||
<span>∞</span>
|
∞
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray text-sm">|</span>
|
<span class="separator">|</span>
|
||||||
''', html)
|
''', html)
|
||||||
|
|
||||||
def assertBookmarkActions(self, html: str, bookmark: Bookmark):
|
def assertBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||||
@@ -52,8 +52,7 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
# Edit link
|
# Edit link
|
||||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
<a href="{edit_url}?return_url=/test"
|
<a href="{edit_url}?return_url=/test">Edit</a>
|
||||||
class="btn btn-link btn-sm">Edit</a>
|
|
||||||
''', html, count=count)
|
''', html, count=count)
|
||||||
# Archive link
|
# Archive link
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
@@ -74,8 +73,8 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
|
|
||||||
def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
|
def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
<span class="text-gray text-sm">Shared by
|
<span>Shared by
|
||||||
<a class="text-gray" href="?user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
<a href="?user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||||
</span>
|
</span>
|
||||||
''', html, count=count)
|
''', html, count=count)
|
||||||
|
|
||||||
@@ -90,6 +89,47 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
<img src="/static/{bookmark.favicon_file}" alt="">
|
<img src="/static/{bookmark.favicon_file}" alt="">
|
||||||
''', html, count=count)
|
''', html, count=count)
|
||||||
|
|
||||||
|
def assertBookmarkURLCount(self, html: str, bookmark: Bookmark, link_target: str = '_blank', count=0):
|
||||||
|
self.assertInHTML(f'''
|
||||||
|
<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 assertNotes(self, html: str, notes_html: str, count=1):
|
||||||
|
self.assertInHTML(f'''
|
||||||
|
<div class="notes bg-gray text-gray-dark">
|
||||||
|
<div class="notes-content">
|
||||||
|
{notes_html}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
''', html, count=count)
|
||||||
|
|
||||||
|
def assertNotesToggle(self, html: str, count=1):
|
||||||
|
self.assertInHTML(f'''
|
||||||
|
<button class="btn btn-link btn-sm toggle-notes" title="Toggle notes">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-notes" width="16" height="16"
|
||||||
|
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path>
|
||||||
|
<path d="M9 7l6 0"></path>
|
||||||
|
<path d="M9 11l6 0"></path>
|
||||||
|
<path d="M9 15l4 0"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Notes</span>
|
||||||
|
</button>
|
||||||
|
''', html, count=count)
|
||||||
|
|
||||||
def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test') -> str:
|
def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test') -> str:
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
request = rf.get(url)
|
request = rf.get(url)
|
||||||
@@ -218,8 +258,8 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
html = self.render_default_template([bookmark], url='/test?q=foo')
|
html = self.render_default_template([bookmark], url='/test?q=foo')
|
||||||
|
|
||||||
self.assertInHTML(f'''
|
self.assertInHTML(f'''
|
||||||
<span class="text-gray text-sm">Shared by
|
<span>Shared by
|
||||||
<a class="text-gray" href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
<a href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||||
</span>
|
</span>
|
||||||
''', html)
|
''', html)
|
||||||
|
|
||||||
@@ -252,3 +292,113 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
html = self.render_default_template([bookmark])
|
html = self.render_default_template([bookmark])
|
||||||
|
|
||||||
self.assertFaviconHidden(html, 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)
|
||||||
|
|
||||||
|
def test_without_notes(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
html = self.render_default_template([bookmark])
|
||||||
|
|
||||||
|
self.assertNotes(html, '', 0)
|
||||||
|
self.assertNotesToggle(html, 0)
|
||||||
|
|
||||||
|
def test_with_notes(self):
|
||||||
|
bookmark = self.setup_bookmark(notes='Test note')
|
||||||
|
html = self.render_default_template([bookmark])
|
||||||
|
|
||||||
|
note_html = '<p>Test note</p>'
|
||||||
|
self.assertNotes(html, note_html, 1)
|
||||||
|
|
||||||
|
def test_note_renders_markdown(self):
|
||||||
|
bookmark = self.setup_bookmark(notes='**Example:** `print("Hello world!")`')
|
||||||
|
html = self.render_default_template([bookmark])
|
||||||
|
|
||||||
|
note_html = '<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
|
||||||
|
self.assertNotes(html, note_html, 1)
|
||||||
|
|
||||||
|
def test_note_cleans_html(self):
|
||||||
|
bookmark = self.setup_bookmark(notes='<script>alert("test")</script>')
|
||||||
|
html = self.render_default_template([bookmark])
|
||||||
|
|
||||||
|
note_html = '<script>alert("test")</script>'
|
||||||
|
self.assertNotes(html, note_html, 1)
|
||||||
|
|
||||||
|
def test_notes_are_hidden_initially_by_default(self):
|
||||||
|
html = self.render_default_template([])
|
||||||
|
|
||||||
|
self.assertInHTML("""
|
||||||
|
<ul class="bookmark-list"></ul>
|
||||||
|
""", html)
|
||||||
|
|
||||||
|
def test_notes_are_hidden_initially_with_permanent_notes_disabled(self):
|
||||||
|
profile = self.get_or_create_test_user().profile
|
||||||
|
profile.permanent_notes = False
|
||||||
|
profile.save()
|
||||||
|
html = self.render_default_template([])
|
||||||
|
|
||||||
|
self.assertInHTML("""
|
||||||
|
<ul class="bookmark-list"></ul>
|
||||||
|
""", html)
|
||||||
|
|
||||||
|
def test_notes_are_visible_initially_with_permanent_notes_enabled(self):
|
||||||
|
profile = self.get_or_create_test_user().profile
|
||||||
|
profile.permanent_notes = True
|
||||||
|
profile.save()
|
||||||
|
html = self.render_default_template([])
|
||||||
|
|
||||||
|
self.assertInHTML("""
|
||||||
|
<ul class="bookmark-list show-notes"></ul>
|
||||||
|
""", html)
|
||||||
|
|
||||||
|
def test_toggle_notes_is_visible_by_default(self):
|
||||||
|
bookmark = self.setup_bookmark(notes='Test note')
|
||||||
|
html = self.render_default_template([bookmark])
|
||||||
|
|
||||||
|
self.assertNotesToggle(html, 1)
|
||||||
|
|
||||||
|
def test_toggle_notes_is_visible_with_permanent_notes_disabled(self):
|
||||||
|
profile = self.get_or_create_test_user().profile
|
||||||
|
profile.permanent_notes = False
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
bookmark = self.setup_bookmark(notes='Test note')
|
||||||
|
html = self.render_default_template([bookmark])
|
||||||
|
|
||||||
|
self.assertNotesToggle(html, 1)
|
||||||
|
|
||||||
|
def test_toggle_notes_is_hidden_with_permanent_notes_enabled(self):
|
||||||
|
profile = self.get_or_create_test_user().profile
|
||||||
|
profile.permanent_notes = True
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
bookmark = self.setup_bookmark(notes='Test note')
|
||||||
|
html = self.render_default_template([bookmark])
|
||||||
|
|
||||||
|
self.assertNotesToggle(html, 0)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark_data = Bookmark(url='https://example.com',
|
bookmark_data = Bookmark(url='https://example.com',
|
||||||
title='Updated Title',
|
title='Updated Title',
|
||||||
description='Updated description',
|
description='Updated description',
|
||||||
|
notes='Updated notes',
|
||||||
unread=True,
|
unread=True,
|
||||||
shared=True,
|
shared=True,
|
||||||
is_archived=True)
|
is_archived=True)
|
||||||
@@ -55,6 +56,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(updated_bookmark.id, original_bookmark.id)
|
self.assertEqual(updated_bookmark.id, original_bookmark.id)
|
||||||
self.assertEqual(updated_bookmark.title, bookmark_data.title)
|
self.assertEqual(updated_bookmark.title, bookmark_data.title)
|
||||||
self.assertEqual(updated_bookmark.description, bookmark_data.description)
|
self.assertEqual(updated_bookmark.description, bookmark_data.description)
|
||||||
|
self.assertEqual(updated_bookmark.notes, bookmark_data.notes)
|
||||||
self.assertEqual(updated_bookmark.unread, bookmark_data.unread)
|
self.assertEqual(updated_bookmark.unread, bookmark_data.unread)
|
||||||
self.assertEqual(updated_bookmark.shared, bookmark_data.shared)
|
self.assertEqual(updated_bookmark.shared, bookmark_data.shared)
|
||||||
# Saving a duplicate bookmark should not modify archive flag - right?
|
# Saving a duplicate bookmark should not modify archive flag - right?
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.db.models import QuerySet
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from bookmarks import queries
|
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.tests.helpers import BookmarkFactoryMixin, random_sentence
|
||||||
from bookmarks.utils import unique
|
from bookmarks.utils import unique
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ User = get_user_model()
|
|||||||
|
|
||||||
|
|
||||||
class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
|
def setUp(self):
|
||||||
|
self.profile = self.get_or_create_test_user().profile
|
||||||
|
|
||||||
def setup_bookmark_search_data(self) -> None:
|
def setup_bookmark_search_data(self) -> None:
|
||||||
tag1 = self.setup_tag(name='tag1')
|
tag1 = self.setup_tag(name='tag1')
|
||||||
@@ -27,9 +29,15 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.term1_bookmarks = [
|
self.term1_bookmarks = [
|
||||||
self.setup_bookmark(url='http://example.com/term1'),
|
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(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(description=random_sentence(including_word='TERM1')),
|
||||||
|
self.setup_bookmark(notes=random_sentence(including_word='term1')),
|
||||||
|
self.setup_bookmark(notes=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_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.setup_bookmark(website_description=random_sentence(including_word='TERM1')),
|
||||||
]
|
]
|
||||||
self.term1_term2_bookmarks = [
|
self.term1_term2_bookmarks = [
|
||||||
self.setup_bookmark(url='http://example.com/term1/term2'),
|
self.setup_bookmark(url='http://example.com/term1/term2'),
|
||||||
@@ -49,6 +57,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(website_title=random_sentence(), tags=[tag1]),
|
self.setup_bookmark(website_title=random_sentence(), tags=[tag1]),
|
||||||
self.setup_bookmark(website_description=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.term1_tag1_bookmarks = [
|
||||||
self.setup_bookmark(url='http://example.com/term1', tags=[tag1]),
|
self.setup_bookmark(url='http://example.com/term1', tags=[tag1]),
|
||||||
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1]),
|
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1]),
|
||||||
@@ -76,9 +91,15 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.term1_bookmarks = [
|
self.term1_bookmarks = [
|
||||||
self.setup_bookmark(url='http://example.com/term1', tags=[self.setup_tag()]),
|
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(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(description=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
|
||||||
|
self.setup_bookmark(notes=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||||
|
self.setup_bookmark(notes=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_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.setup_bookmark(website_description=random_sentence(including_word='TERM1'), tags=[self.setup_tag()]),
|
||||||
]
|
]
|
||||||
self.term1_term2_bookmarks = [
|
self.term1_term2_bookmarks = [
|
||||||
self.setup_bookmark(url='http://example.com/term1/term2', tags=[self.setup_tag()]),
|
self.setup_bookmark(url='http://example.com/term1/term2', tags=[self.setup_tag()]),
|
||||||
@@ -102,6 +123,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(website_title=random_sentence(), tags=[tag1, self.setup_tag()]),
|
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.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.term1_tag1_bookmarks = [
|
||||||
self.setup_bookmark(url='http://example.com/term1', tags=[tag1, self.setup_tag()]),
|
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()]),
|
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1, self.setup_tag()]),
|
||||||
@@ -135,12 +163,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmarks_should_return_all_for_empty_query(self):
|
def test_query_bookmarks_should_return_all_for_empty_query(self):
|
||||||
self.setup_bookmark_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.other_bookmarks,
|
self.other_bookmarks,
|
||||||
self.term1_bookmarks,
|
self.term1_bookmarks,
|
||||||
self.term1_term2_bookmarks,
|
self.term1_term2_bookmarks,
|
||||||
self.tag1_bookmarks,
|
self.tag1_bookmarks,
|
||||||
|
self.tag1_as_term_bookmarks,
|
||||||
self.term1_tag1_bookmarks,
|
self.term1_tag1_bookmarks,
|
||||||
self.tag2_bookmarks,
|
self.tag2_bookmarks,
|
||||||
self.tag1_tag2_bookmarks
|
self.tag1_tag2_bookmarks
|
||||||
@@ -149,7 +178,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmarks_should_search_single_term(self):
|
def test_query_bookmarks_should_search_single_term(self):
|
||||||
self.setup_bookmark_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.term1_bookmarks,
|
self.term1_bookmarks,
|
||||||
self.term1_term2_bookmarks,
|
self.term1_term2_bookmarks,
|
||||||
@@ -159,63 +188,101 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmarks_should_search_multiple_terms(self):
|
def test_query_bookmarks_should_search_multiple_terms(self):
|
||||||
self.setup_bookmark_search_data()
|
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])
|
self.assertQueryResult(query, [self.term1_term2_bookmarks])
|
||||||
|
|
||||||
def test_query_bookmarks_should_search_single_tag(self):
|
def test_query_bookmarks_should_search_single_tag(self):
|
||||||
self.setup_bookmark_search_data()
|
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])
|
self.assertQueryResult(query, [self.tag1_bookmarks, self.tag1_tag2_bookmarks, self.term1_tag1_bookmarks])
|
||||||
|
|
||||||
def test_query_bookmarks_should_search_multiple_tags(self):
|
def test_query_bookmarks_should_search_multiple_tags(self):
|
||||||
self.setup_bookmark_search_data()
|
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])
|
self.assertQueryResult(query, [self.tag1_tag2_bookmarks])
|
||||||
|
|
||||||
def test_query_bookmarks_should_search_multiple_tags_ignoring_casing(self):
|
def test_query_bookmarks_should_search_multiple_tags_ignoring_casing(self):
|
||||||
self.setup_bookmark_search_data()
|
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])
|
self.assertQueryResult(query, [self.tag1_tag2_bookmarks])
|
||||||
|
|
||||||
def test_query_bookmarks_should_search_terms_and_tags_combined(self):
|
def test_query_bookmarks_should_search_terms_and_tags_combined(self):
|
||||||
self.setup_bookmark_search_data()
|
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])
|
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):
|
def test_query_bookmarks_should_return_no_matches(self):
|
||||||
self.setup_bookmark_search_data()
|
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, [])
|
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, [])
|
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, [])
|
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, [])
|
self.assertQueryResult(query, [])
|
||||||
|
|
||||||
# Unused tag
|
# 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, [])
|
self.assertQueryResult(query, [])
|
||||||
|
|
||||||
# Unused tag combined with tag that is used
|
# 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, [])
|
self.assertQueryResult(query, [])
|
||||||
|
|
||||||
# Unused tag combined with term that is used
|
# 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, [])
|
self.assertQueryResult(query, [])
|
||||||
|
|
||||||
def test_query_bookmarks_should_not_return_archived_bookmarks(self):
|
def test_query_bookmarks_should_not_return_archived_bookmarks(self):
|
||||||
@@ -225,7 +292,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(is_archived=True)
|
self.setup_bookmark(is_archived=True)
|
||||||
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]])
|
self.assertQueryResult(query, [[bookmark1, bookmark2]])
|
||||||
|
|
||||||
@@ -236,7 +303,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark()
|
self.setup_bookmark()
|
||||||
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]])
|
self.assertQueryResult(query, [[bookmark1, bookmark2]])
|
||||||
|
|
||||||
@@ -251,7 +318,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(user=other_user)
|
self.setup_bookmark(user=other_user)
|
||||||
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])
|
self.assertQueryResult(query, [owned_bookmarks])
|
||||||
|
|
||||||
@@ -266,7 +333,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(is_archived=True, user=other_user)
|
self.setup_bookmark(is_archived=True, user=other_user)
|
||||||
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])
|
self.assertQueryResult(query, [owned_bookmarks])
|
||||||
|
|
||||||
@@ -276,7 +343,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(tags=[tag])
|
self.setup_bookmark(tags=[tag])
|
||||||
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])
|
self.assertCountEqual(list(query), [untagged_bookmark])
|
||||||
|
|
||||||
def test_query_bookmarks_untagged_should_be_combinable_with_search_terms(self):
|
def test_query_bookmarks_untagged_should_be_combinable_with_search_terms(self):
|
||||||
@@ -285,7 +352,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(title='term2')
|
self.setup_bookmark(title='term2')
|
||||||
self.setup_bookmark(tags=[tag])
|
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])
|
self.assertCountEqual(list(query), [untagged_bookmark])
|
||||||
|
|
||||||
def test_query_bookmarks_untagged_should_not_be_combinable_with_tags(self):
|
def test_query_bookmarks_untagged_should_not_be_combinable_with_tags(self):
|
||||||
@@ -294,7 +361,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(tags=[tag])
|
self.setup_bookmark(tags=[tag])
|
||||||
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), [])
|
self.assertCountEqual(list(query), [])
|
||||||
|
|
||||||
def test_query_archived_bookmarks_untagged_should_return_untagged_bookmarks_only(self):
|
def test_query_archived_bookmarks_untagged_should_return_untagged_bookmarks_only(self):
|
||||||
@@ -303,7 +370,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(is_archived=True, tags=[tag])
|
self.setup_bookmark(is_archived=True, tags=[tag])
|
||||||
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])
|
self.assertCountEqual(list(query), [untagged_bookmark])
|
||||||
|
|
||||||
def test_query_archived_bookmarks_untagged_should_be_combinable_with_search_terms(self):
|
def test_query_archived_bookmarks_untagged_should_be_combinable_with_search_terms(self):
|
||||||
@@ -312,7 +379,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(is_archived=True, title='term2')
|
self.setup_bookmark(is_archived=True, title='term2')
|
||||||
self.setup_bookmark(is_archived=True, tags=[tag])
|
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])
|
self.assertCountEqual(list(query), [untagged_bookmark])
|
||||||
|
|
||||||
def test_query_archived_bookmarks_untagged_should_not_be_combinable_with_tags(self):
|
def test_query_archived_bookmarks_untagged_should_not_be_combinable_with_tags(self):
|
||||||
@@ -321,7 +388,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(is_archived=True, tags=[tag])
|
self.setup_bookmark(is_archived=True, tags=[tag])
|
||||||
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), [])
|
self.assertCountEqual(list(query), [])
|
||||||
|
|
||||||
def test_query_bookmarks_unread_should_return_unread_bookmarks_only(self):
|
def test_query_bookmarks_unread_should_return_unread_bookmarks_only(self):
|
||||||
@@ -334,7 +401,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark()
|
self.setup_bookmark()
|
||||||
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)
|
self.assertCountEqual(list(query), unread_bookmarks)
|
||||||
|
|
||||||
def test_query_archived_bookmarks_unread_should_return_unread_bookmarks_only(self):
|
def test_query_archived_bookmarks_unread_should_return_unread_bookmarks_only(self):
|
||||||
@@ -347,13 +414,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(is_archived=True)
|
self.setup_bookmark(is_archived=True)
|
||||||
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)
|
self.assertCountEqual(list(query), unread_bookmarks)
|
||||||
|
|
||||||
def test_query_bookmark_tags_should_return_all_tags_for_empty_query(self):
|
def test_query_bookmark_tags_should_return_all_tags_for_empty_query(self):
|
||||||
self.setup_tag_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.get_tags_from_bookmarks(self.other_bookmarks),
|
self.get_tags_from_bookmarks(self.other_bookmarks),
|
||||||
@@ -368,7 +435,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmark_tags_should_search_single_term(self):
|
def test_query_bookmark_tags_should_search_single_term(self):
|
||||||
self.setup_tag_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.get_tags_from_bookmarks(self.term1_bookmarks),
|
self.get_tags_from_bookmarks(self.term1_bookmarks),
|
||||||
@@ -379,7 +446,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmark_tags_should_search_multiple_terms(self):
|
def test_query_bookmark_tags_should_search_multiple_terms(self):
|
||||||
self.setup_tag_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
|
self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
|
||||||
@@ -388,7 +455,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmark_tags_should_search_single_tag(self):
|
def test_query_bookmark_tags_should_search_single_tag(self):
|
||||||
self.setup_tag_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.get_tags_from_bookmarks(self.tag1_bookmarks),
|
self.get_tags_from_bookmarks(self.tag1_bookmarks),
|
||||||
@@ -399,7 +466,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmark_tags_should_search_multiple_tags(self):
|
def test_query_bookmark_tags_should_search_multiple_tags(self):
|
||||||
self.setup_tag_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||||
@@ -408,7 +475,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmark_tags_should_search_multiple_tags_ignoring_casing(self):
|
def test_query_bookmark_tags_should_search_multiple_tags_ignoring_casing(self):
|
||||||
self.setup_tag_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||||
@@ -417,37 +484,75 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_query_bookmark_tags_should_search_term_and_tag_combined(self):
|
def test_query_bookmark_tags_should_search_term_and_tag_combined(self):
|
||||||
self.setup_tag_search_data()
|
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.assertQueryResult(query, [
|
||||||
self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
|
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):
|
def test_query_bookmark_tags_should_return_no_matches(self):
|
||||||
self.setup_tag_search_data()
|
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, [])
|
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, [])
|
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, [])
|
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, [])
|
self.assertQueryResult(query, [])
|
||||||
|
|
||||||
# Unused tag
|
# 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, [])
|
self.assertQueryResult(query, [])
|
||||||
|
|
||||||
# Unused tag combined with tag that is used
|
# 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, [])
|
self.assertQueryResult(query, [])
|
||||||
|
|
||||||
# Unused tag combined with term that is used
|
# 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, [])
|
self.assertQueryResult(query, [])
|
||||||
|
|
||||||
def test_query_bookmark_tags_should_return_tags_for_unarchived_bookmarks_only(self):
|
def test_query_bookmark_tags_should_return_tags_for_unarchived_bookmarks_only(self):
|
||||||
@@ -457,7 +562,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark()
|
self.setup_bookmark()
|
||||||
self.setup_bookmark(is_archived=True, tags=[tag2])
|
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]])
|
self.assertQueryResult(query, [[tag1]])
|
||||||
|
|
||||||
@@ -467,7 +572,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(tags=[tag])
|
self.setup_bookmark(tags=[tag])
|
||||||
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]])
|
self.assertQueryResult(query, [[tag]])
|
||||||
|
|
||||||
@@ -478,7 +583,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark()
|
self.setup_bookmark()
|
||||||
self.setup_bookmark(is_archived=True, tags=[tag2])
|
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]])
|
self.assertQueryResult(query, [[tag2]])
|
||||||
|
|
||||||
@@ -488,7 +593,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(is_archived=True, tags=[tag])
|
self.setup_bookmark(is_archived=True, tags=[tag])
|
||||||
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]])
|
self.assertQueryResult(query, [[tag]])
|
||||||
|
|
||||||
@@ -503,7 +608,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)])
|
||||||
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)])
|
self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
|
||||||
|
|
||||||
@@ -518,7 +623,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)])
|
||||||
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)])
|
self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
|
||||||
|
|
||||||
@@ -529,13 +634,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(title='term1', tags=[tag])
|
self.setup_bookmark(title='term1', tags=[tag])
|
||||||
self.setup_bookmark(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), [])
|
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), [])
|
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), [])
|
self.assertCountEqual(list(query), [])
|
||||||
|
|
||||||
def test_query_archived_bookmark_tags_untagged_should_never_return_any_tags(self):
|
def test_query_archived_bookmark_tags_untagged_should_never_return_any_tags(self):
|
||||||
@@ -545,13 +650,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(is_archived=True, title='term1', tags=[tag])
|
self.setup_bookmark(is_archived=True, title='term1', tags=[tag])
|
||||||
self.setup_bookmark(is_archived=True, 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), [])
|
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), [])
|
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), [])
|
self.assertCountEqual(list(query), [])
|
||||||
|
|
||||||
def test_query_shared_bookmarks(self):
|
def test_query_shared_bookmarks(self):
|
||||||
@@ -574,14 +679,14 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(user=user4, shared=True, tags=[tag]),
|
self.setup_bookmark(user=user4, shared=True, tags=[tag]),
|
||||||
|
|
||||||
# Should return shared bookmarks from all users
|
# 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])
|
self.assertQueryResult(query_set, [shared_bookmarks])
|
||||||
|
|
||||||
# Should respect search query
|
# 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]]])
|
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]]])
|
self.assertQueryResult(query_set, [[shared_bookmarks[2]]])
|
||||||
|
|
||||||
def test_query_shared_bookmark_tags(self):
|
def test_query_shared_bookmark_tags(self):
|
||||||
@@ -605,7 +710,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(user=user3, shared=False, tags=[self.setup_tag(user=user3)]),
|
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)]),
|
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])
|
self.assertQueryResult(query_set, [shared_tags])
|
||||||
|
|
||||||
@@ -630,9 +735,9 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.setup_bookmark(user=users_without_shared_bookmarks[2], shared=True),
|
self.setup_bookmark(user=users_without_shared_bookmarks[2], shared=True),
|
||||||
|
|
||||||
# Should return users with shared bookmarks
|
# 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])
|
self.assertQueryResult(query_set, [users_with_shared_bookmarks])
|
||||||
|
|
||||||
# Should respect search query
|
# 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]]])
|
self.assertQueryResult(query_set, [[users_with_shared_bookmarks[0]]])
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED,
|
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED,
|
||||||
'enable_sharing': False,
|
'enable_sharing': False,
|
||||||
'enable_favicons': False,
|
'enable_favicons': False,
|
||||||
|
'tag_search': UserProfile.TAG_SEARCH_STRICT,
|
||||||
|
'display_url': False,
|
||||||
|
'permanent_notes': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {**form_data, **overrides}
|
return {**form_data, **overrides}
|
||||||
@@ -52,6 +55,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
|
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
|
||||||
'enable_sharing': True,
|
'enable_sharing': True,
|
||||||
'enable_favicons': True,
|
'enable_favicons': True,
|
||||||
|
'tag_search': UserProfile.TAG_SEARCH_LAX,
|
||||||
|
'display_url': True,
|
||||||
|
'permanent_notes': True,
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
@@ -65,6 +71,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(self.user.profile.web_archive_integration, form_data['web_archive_integration'])
|
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_sharing, form_data['enable_sharing'])
|
||||||
self.assertEqual(self.user.profile.enable_favicons, form_data['enable_favicons'])
|
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.assertEqual(self.user.profile.permanent_notes, form_data['permanent_notes'])
|
||||||
self.assertInHTML('''
|
self.assertInHTML('''
|
||||||
<p class="form-input-hint">Profile updated</p>
|
<p class="form-input-hint">Profile updated</p>
|
||||||
''', html)
|
''', html)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import List
|
|||||||
from django.template import Template, RequestContext
|
from django.template import Template, RequestContext
|
||||||
from django.test import TestCase, RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
|
|
||||||
from bookmarks.models import Tag
|
from bookmarks.models import Tag, UserProfile
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
|
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
|
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
request = rf.get(url)
|
request = rf.get(url)
|
||||||
|
request.user = self.get_or_create_test_user()
|
||||||
context = RequestContext(request, {
|
context = RequestContext(request, {
|
||||||
'request': request,
|
'request': request,
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
@@ -118,6 +119,36 @@ class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
</a>
|
</a>
|
||||||
''', rendered_template)
|
''', 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):
|
def test_selected_tags_ignore_casing_when_removing_query_part(self):
|
||||||
tags = [
|
tags = [
|
||||||
self.setup_tag(name='TEST'),
|
self.setup_tag(name='TEST'),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
@@ -9,7 +10,7 @@ from django.shortcuts import render
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from bookmarks import queries
|
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, \
|
from bookmarks.services.bookmarks import create_bookmark, update_bookmark, archive_bookmark, archive_bookmarks, \
|
||||||
unarchive_bookmark, unarchive_bookmarks, delete_bookmarks, tag_bookmarks, untag_bookmarks
|
unarchive_bookmark, unarchive_bookmarks, delete_bookmarks, tag_bookmarks, untag_bookmarks
|
||||||
from bookmarks.utils import get_safe_return_url
|
from bookmarks.utils import get_safe_return_url
|
||||||
@@ -20,8 +21,8 @@ _default_page_size = 30
|
|||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
filters = BookmarkFilters(request)
|
filters = BookmarkFilters(request)
|
||||||
query_set = queries.query_bookmarks(request.user, filters.query)
|
query_set = queries.query_bookmarks(request.user, request.user.profile, filters.query)
|
||||||
tags = queries.query_bookmark_tags(request.user, filters.query)
|
tags = queries.query_bookmark_tags(request.user, request.user.profile, filters.query)
|
||||||
base_url = reverse('bookmarks:index')
|
base_url = reverse('bookmarks:index')
|
||||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||||
return render(request, 'bookmarks/index.html', context)
|
return render(request, 'bookmarks/index.html', context)
|
||||||
@@ -30,8 +31,8 @@ def index(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def archived(request):
|
def archived(request):
|
||||||
filters = BookmarkFilters(request)
|
filters = BookmarkFilters(request)
|
||||||
query_set = queries.query_archived_bookmarks(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, filters.query)
|
tags = queries.query_archived_bookmark_tags(request.user, request.user.profile, filters.query)
|
||||||
base_url = reverse('bookmarks:archived')
|
base_url = reverse('bookmarks:archived')
|
||||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||||
return render(request, 'bookmarks/archive.html', context)
|
return render(request, 'bookmarks/archive.html', context)
|
||||||
@@ -41,27 +42,23 @@ def archived(request):
|
|||||||
def shared(request):
|
def shared(request):
|
||||||
filters = BookmarkFilters(request)
|
filters = BookmarkFilters(request)
|
||||||
user = User.objects.filter(username=filters.user).first()
|
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)
|
||||||
tags = queries.query_shared_bookmark_tags(user, filters.query)
|
tags = queries.query_shared_bookmark_tags(user, request.user.profile, filters.query)
|
||||||
users = queries.query_shared_bookmark_users(filters.query)
|
users = queries.query_shared_bookmark_users(request.user.profile, filters.query)
|
||||||
base_url = reverse('bookmarks:shared')
|
base_url = reverse('bookmarks:shared')
|
||||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||||
context['users'] = users
|
context['users'] = users
|
||||||
return render(request, 'bookmarks/shared.html', context)
|
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)
|
parsed_query = queries.parse_query_string(query_string)
|
||||||
tag_names = parsed_query['tag_names']
|
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 [tag for tag in tags if tag.name.lower() in tag_names]
|
||||||
return []
|
|
||||||
|
|
||||||
condition = Q()
|
|
||||||
for tag_name in parsed_query['tag_names']:
|
|
||||||
condition = condition | Q(name__iexact=tag_name)
|
|
||||||
|
|
||||||
return list(tags.filter(condition))
|
|
||||||
|
|
||||||
|
|
||||||
def get_bookmark_view_context(request: WSGIRequest,
|
def get_bookmark_view_context(request: WSGIRequest,
|
||||||
@@ -72,7 +69,8 @@ def get_bookmark_view_context(request: WSGIRequest,
|
|||||||
page = request.GET.get('page')
|
page = request.GET.get('page')
|
||||||
paginator = Paginator(query_set, _default_page_size)
|
paginator = Paginator(query_set, _default_page_size)
|
||||||
bookmarks = paginator.get_page(page)
|
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, this avoids n+1 queries when accessing fields in templates
|
||||||
prefetch_related_objects(bookmarks.object_list, 'owner', 'tags')
|
prefetch_related_objects(bookmarks.object_list, 'owner', 'tags')
|
||||||
return_url = generate_return_url(base_url, page, filters)
|
return_url = generate_return_url(base_url, page, filters)
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ def bookmark_import(request):
|
|||||||
def bookmark_export(request):
|
def bookmark_export(request):
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
bookmarks = list(query_bookmarks(request.user, ''))
|
bookmarks = list(query_bookmarks(request.user, request.user.profile, ''))
|
||||||
# Prefetch tags to prevent n+1 queries
|
# Prefetch tags to prevent n+1 queries
|
||||||
prefetch_related_objects(bookmarks, 'tags')
|
prefetch_related_objects(bookmarks, 'tags')
|
||||||
file_content = exporter.export_netscape_html(bookmarks)
|
file_content = exporter.export_netscape_html(bookmarks)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ Example response:
|
|||||||
"url": "https://example.com",
|
"url": "https://example.com",
|
||||||
"title": "Example title",
|
"title": "Example title",
|
||||||
"description": "Example description",
|
"description": "Example description",
|
||||||
|
"notes": "Example notes",
|
||||||
"website_title": "Website title",
|
"website_title": "Website title",
|
||||||
"website_description": "Website description",
|
"website_description": "Website description",
|
||||||
"is_archived": false,
|
"is_archived": false,
|
||||||
@@ -96,6 +97,7 @@ Example payload:
|
|||||||
"url": "https://example.com",
|
"url": "https://example.com",
|
||||||
"title": "Example title",
|
"title": "Example title",
|
||||||
"description": "Example description",
|
"description": "Example description",
|
||||||
|
"notes": "Example notes",
|
||||||
"is_archived": false,
|
"is_archived": false,
|
||||||
"unread": false,
|
"unread": false,
|
||||||
"shared": false,
|
"shared": false,
|
||||||
|
|||||||
@@ -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).
|
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`
|
### `LD_DB_ENGINE`
|
||||||
|
|
||||||
Values: `postgres` or `sqlite` | Default = `sqlite`
|
Values: `postgres` or `sqlite` | Default = `sqlite`
|
||||||
@@ -150,6 +156,12 @@ Values: `Integer` | Default = None
|
|||||||
The port of the database server.
|
The port of the database server.
|
||||||
Should use the default port if left empty, for example `5432` for PostgresSQL.
|
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`
|
### `LD_FAVICON_PROVIDER`
|
||||||
|
|
||||||
Values: `String` | Default = `https://t1.gstatic.com/faviconV2?url={url}&client=SOCIAL&type=FAVICON`
|
Values: `String` | Default = `https://t1.gstatic.com/faviconV2?url={url}&client=SOCIAL&type=FAVICON`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "linkding",
|
"name": "linkding",
|
||||||
"version": "1.17.2",
|
"version": "1.19.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
asgiref==3.5.2
|
asgiref==3.5.2
|
||||||
beautifulsoup4==4.11.1
|
beautifulsoup4==4.11.1
|
||||||
|
bleach==6.0.0
|
||||||
|
bleach-allowlist==1.0.3
|
||||||
certifi==2022.12.7
|
certifi==2022.12.7
|
||||||
charset-normalizer==2.1.1
|
charset-normalizer==2.1.1
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
confusable-homoglyphs==3.2.0
|
confusable-homoglyphs==3.2.0
|
||||||
Django==4.1.7
|
Django==4.1.9
|
||||||
django-generate-secret-key==1.0.2
|
django-generate-secret-key==1.0.2
|
||||||
django-registration==3.3
|
django-registration==3.3
|
||||||
django-sass-processor==1.2.1
|
django-sass-processor==1.2.1
|
||||||
@@ -12,14 +14,16 @@ django-widget-tweaks==1.4.12
|
|||||||
django4-background-tasks==1.2.7
|
django4-background-tasks==1.2.7
|
||||||
djangorestframework==3.13.1
|
djangorestframework==3.13.1
|
||||||
idna==3.3
|
idna==3.3
|
||||||
|
Markdown==3.4.3
|
||||||
psycopg2==2.9.5
|
psycopg2==2.9.5
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
pytz==2022.2.1
|
pytz==2022.2.1
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
soupsieve==2.3.2.post1
|
soupsieve==2.3.2.post1
|
||||||
sqlparse==0.4.2
|
sqlparse==0.4.4
|
||||||
supervisor==4.2.4
|
supervisor==4.2.4
|
||||||
typing-extensions==3.10.0.0
|
typing-extensions==3.10.0.0
|
||||||
urllib3==1.26.11
|
urllib3==1.26.11
|
||||||
uWSGI==2.0.20
|
uWSGI==2.0.20
|
||||||
waybackpy==3.0.6
|
waybackpy==3.0.6
|
||||||
|
webencodings==0.5.1
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
asgiref==3.5.2
|
asgiref==3.5.2
|
||||||
beautifulsoup4==4.11.1
|
beautifulsoup4==4.11.1
|
||||||
|
bleach==6.0.0
|
||||||
|
bleach-allowlist==1.0.3
|
||||||
certifi==2022.12.7
|
certifi==2022.12.7
|
||||||
charset-normalizer==2.1.1
|
charset-normalizer==2.1.1
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
confusable-homoglyphs==3.2.0
|
confusable-homoglyphs==3.2.0
|
||||||
coverage==5.5
|
coverage==5.5
|
||||||
Django==4.1.7
|
Django==4.1.9
|
||||||
django-appconf==1.0.5
|
django-appconf==1.0.5
|
||||||
django-compressor==4.1
|
django-compressor==4.1
|
||||||
django-debug-toolbar==3.6.0
|
django-debug-toolbar==3.6.0
|
||||||
@@ -18,6 +20,7 @@ djangorestframework==3.13.1
|
|||||||
greenlet==2.0.1
|
greenlet==2.0.1
|
||||||
idna==3.3
|
idna==3.3
|
||||||
libsass==0.21.0
|
libsass==0.21.0
|
||||||
|
Markdown==3.4.3
|
||||||
playwright==1.29.1
|
playwright==1.29.1
|
||||||
psycopg2-binary==2.9.5
|
psycopg2-binary==2.9.5
|
||||||
pyee==9.0.4
|
pyee==9.0.4
|
||||||
@@ -28,7 +31,8 @@ requests==2.28.1
|
|||||||
rjsmin==1.2.0
|
rjsmin==1.2.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
soupsieve==2.3.2.post1
|
soupsieve==2.3.2.post1
|
||||||
sqlparse==0.4.2
|
sqlparse==0.4.4
|
||||||
typing-extensions==3.10.0.0
|
typing-extensions==3.10.0.0
|
||||||
urllib3==1.26.11
|
urllib3==1.26.11
|
||||||
waybackpy==3.0.6
|
waybackpy==3.0.6
|
||||||
|
webencodings==0.5.1
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/2.2/ref/settings/
|
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# 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_USER = os.getenv('LD_DB_USER', 'linkding')
|
||||||
LD_DB_PASSWORD = os.getenv('LD_DB_PASSWORD', None)
|
LD_DB_PASSWORD = os.getenv('LD_DB_PASSWORD', None)
|
||||||
LD_DB_PORT = os.getenv('LD_DB_PORT', 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':
|
if LD_DB_ENGINE == 'postgres':
|
||||||
default_database = {
|
default_database = {
|
||||||
@@ -213,11 +215,13 @@ if LD_DB_ENGINE == 'postgres':
|
|||||||
'PASSWORD': LD_DB_PASSWORD,
|
'PASSWORD': LD_DB_PASSWORD,
|
||||||
'HOST': LD_DB_HOST,
|
'HOST': LD_DB_HOST,
|
||||||
'PORT': LD_DB_PORT,
|
'PORT': LD_DB_PORT,
|
||||||
|
'OPTIONS': LD_DB_OPTIONS,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
default_database = {
|
default_database = {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': os.path.join(BASE_DIR, 'data', 'db.sqlite3'),
|
'NAME': os.path.join(BASE_DIR, 'data', 'db.sqlite3'),
|
||||||
|
'OPTIONS': LD_DB_OPTIONS,
|
||||||
}
|
}
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ INTERNAL_IPS = [
|
|||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Allow access through ngrok
|
||||||
|
CSRF_TRUSTED_ORIGINS = ['https://*.ngrok-free.app']
|
||||||
|
|
||||||
# Enable debug logging
|
# Enable debug logging
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
|
|||||||
@@ -22,4 +22,8 @@ if-env = LD_REQUEST_TIMEOUT
|
|||||||
http-timeout = %(_)
|
http-timeout = %(_)
|
||||||
socket-timeout = %(_)
|
socket-timeout = %(_)
|
||||||
harakiri = %(_)
|
harakiri = %(_)
|
||||||
endif =
|
endif =
|
||||||
|
|
||||||
|
if-env = LD_LOG_X_FORWARDED_FOR
|
||||||
|
log-x-forwarded-for = %(_)
|
||||||
|
endif =
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.17.2
|
1.19.0
|
||||||
|
|||||||
Reference in New Issue
Block a user