From 3af4e07eb6599b268b956c246f1ff661b288ba71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Thu, 18 May 2023 09:06:22 +0200 Subject: [PATCH] Allow searching for tags without hash character (#449) * Allow searching for tags without hash character * Allow removing selected tags without hash * Add more tests --- bookmarks/api/routes.py | 6 +- bookmarks/feeds.py | 2 +- .../migrations/0020_userprofile_tag_search.py | 18 ++ bookmarks/models.py | 11 +- bookmarks/queries.py | 51 +++-- .../templates/bookmarks/bookmark_list.html | 2 +- bookmarks/templates/bookmarks/tag_cloud.html | 6 +- bookmarks/templates/settings/general.html | 9 + bookmarks/templatetags/shared.py | 52 +++-- .../tests/test_bookmark_archived_view.py | 31 +++ bookmarks/tests/test_bookmark_index_view.py | 33 ++- bookmarks/tests/test_queries.py | 211 +++++++++++++----- bookmarks/tests/test_settings_general_view.py | 3 + bookmarks/tests/test_tag_cloud_tag.py | 33 ++- bookmarks/views/bookmarks.py | 34 ++- bookmarks/views/settings.py | 2 +- 16 files changed, 367 insertions(+), 137 deletions(-) create mode 100644 bookmarks/migrations/0020_userprofile_tag_search.py diff --git a/bookmarks/api/routes.py b/bookmarks/api/routes.py index a4cae8a..e99612d 100644 --- a/bookmarks/api/routes.py +++ b/bookmarks/api/routes.py @@ -23,7 +23,7 @@ class BookmarkViewSet(viewsets.GenericViewSet, # For list action, use query set that applies search and tag projections if self.action == 'list': query_string = self.request.GET.get('q') - return queries.query_bookmarks(user, query_string) + return queries.query_bookmarks(user, user.profile, query_string) # For single entity actions use default query set without projections return Bookmark.objects.all().filter(owner=user) @@ -35,7 +35,7 @@ class BookmarkViewSet(viewsets.GenericViewSet, def archived(self, request): user = request.user query_string = request.GET.get('q') - query_set = queries.query_archived_bookmarks(user, query_string) + query_set = queries.query_archived_bookmarks(user, user.profile, query_string) page = self.paginate_queryset(query_set) serializer = self.get_serializer_class() data = serializer(page, many=True).data @@ -45,7 +45,7 @@ class BookmarkViewSet(viewsets.GenericViewSet, def shared(self, request): filters = BookmarkFilters(request) user = User.objects.filter(username=filters.user).first() - query_set = queries.query_shared_bookmarks(user, filters.query) + query_set = queries.query_shared_bookmarks(user, request.user.profile, filters.query) page = self.paginate_queryset(query_set) serializer = self.get_serializer_class() data = serializer(page, many=True).data diff --git a/bookmarks/feeds.py b/bookmarks/feeds.py index 883721f..3097315 100644 --- a/bookmarks/feeds.py +++ b/bookmarks/feeds.py @@ -18,7 +18,7 @@ class BaseBookmarksFeed(Feed): def get_object(self, request, feed_key: str): feed_token = FeedToken.objects.get(key__exact=feed_key) query_string = request.GET.get('q') - query_set = queries.query_bookmarks(feed_token.user, query_string) + query_set = queries.query_bookmarks(feed_token.user, feed_token.user.profile, query_string) return FeedContext(feed_token, query_set) def item_title(self, item: Bookmark): diff --git a/bookmarks/migrations/0020_userprofile_tag_search.py b/bookmarks/migrations/0020_userprofile_tag_search.py new file mode 100644 index 0000000..78711e7 --- /dev/null +++ b/bookmarks/migrations/0020_userprofile_tag_search.py @@ -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), + ), + ] diff --git a/bookmarks/models.py b/bookmarks/models.py index c652474..ecef619 100644 --- a/bookmarks/models.py +++ b/bookmarks/models.py @@ -153,6 +153,12 @@ class UserProfile(models.Model): (WEB_ARCHIVE_INTEGRATION_DISABLED, 'Disabled'), (WEB_ARCHIVE_INTEGRATION_ENABLED, 'Enabled'), ] + TAG_SEARCH_STRICT = 'strict' + TAG_SEARCH_LAX = 'lax' + TAG_SEARCH_CHOICES = [ + (TAG_SEARCH_STRICT, 'Strict'), + (TAG_SEARCH_LAX, 'Lax'), + ] user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE) theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO) bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, blank=False, @@ -161,6 +167,8 @@ class UserProfile(models.Model): default=BOOKMARK_LINK_TARGET_BLANK) web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False, default=WEB_ARCHIVE_INTEGRATION_DISABLED) + tag_search = models.CharField(max_length=10, choices=TAG_SEARCH_CHOICES, blank=False, + default=TAG_SEARCH_STRICT) enable_sharing = models.BooleanField(default=False, null=False) enable_favicons = models.BooleanField(default=False, null=False) @@ -168,7 +176,8 @@ class UserProfile(models.Model): class UserProfileForm(forms.ModelForm): class Meta: model = UserProfile - fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'enable_sharing', 'enable_favicons'] + fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'tag_search', + 'enable_sharing', 'enable_favicons'] @receiver(post_save, sender=get_user_model()) diff --git a/bookmarks/queries.py b/bookmarks/queries.py index 30cca26..3008d09 100644 --- a/bookmarks/queries.py +++ b/bookmarks/queries.py @@ -1,29 +1,29 @@ from typing import Optional from django.contrib.auth.models import User -from django.db.models import Q, QuerySet +from django.db.models import Q, QuerySet, Exists, OuterRef -from bookmarks.models import Bookmark, Tag +from bookmarks.models import Bookmark, Tag, UserProfile from bookmarks.utils import unique -def query_bookmarks(user: User, query_string: str) -> QuerySet: - return _base_bookmarks_query(user, query_string) \ +def query_bookmarks(user: User, profile: UserProfile, query_string: str) -> QuerySet: + return _base_bookmarks_query(user, profile, query_string) \ .filter(is_archived=False) -def query_archived_bookmarks(user: User, query_string: str) -> QuerySet: - return _base_bookmarks_query(user, query_string) \ +def query_archived_bookmarks(user: User, profile: UserProfile, query_string: str) -> QuerySet: + return _base_bookmarks_query(user, profile, query_string) \ .filter(is_archived=True) -def query_shared_bookmarks(user: Optional[User], query_string: str) -> QuerySet: - return _base_bookmarks_query(user, query_string) \ +def query_shared_bookmarks(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet: + return _base_bookmarks_query(user, profile, query_string) \ .filter(shared=True) \ .filter(owner__profile__enable_sharing=True) -def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet: +def _base_bookmarks_query(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet: query_set = Bookmark.objects # Filter for user @@ -35,13 +35,16 @@ def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet: # Filter for search terms and tags for term in query['search_terms']: - query_set = query_set.filter( - Q(title__icontains=term) - | Q(description__icontains=term) - | Q(website_title__icontains=term) - | Q(website_description__icontains=term) - | Q(url__icontains=term) - ) + conditions = Q(title__icontains=term) \ + | Q(description__icontains=term) \ + | Q(website_title__icontains=term) \ + | Q(website_description__icontains=term) \ + | Q(url__icontains=term) + + if profile.tag_search == UserProfile.TAG_SEARCH_LAX: + conditions = conditions | Exists(Bookmark.objects.filter(id=OuterRef('id'), tags__name__iexact=term)) + + query_set = query_set.filter(conditions) for tag_name in query['tag_names']: query_set = query_set.filter( @@ -65,32 +68,32 @@ def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet: return query_set -def query_bookmark_tags(user: User, query_string: str) -> QuerySet: - bookmarks_query = query_bookmarks(user, query_string) +def query_bookmark_tags(user: User, profile: UserProfile, query_string: str) -> QuerySet: + bookmarks_query = query_bookmarks(user, profile, query_string) query_set = Tag.objects.filter(bookmark__in=bookmarks_query) return query_set.distinct() -def query_archived_bookmark_tags(user: User, query_string: str) -> QuerySet: - bookmarks_query = query_archived_bookmarks(user, query_string) +def query_archived_bookmark_tags(user: User, profile: UserProfile, query_string: str) -> QuerySet: + bookmarks_query = query_archived_bookmarks(user, profile, query_string) query_set = Tag.objects.filter(bookmark__in=bookmarks_query) return query_set.distinct() -def query_shared_bookmark_tags(user: Optional[User], query_string: str) -> QuerySet: - bookmarks_query = query_shared_bookmarks(user, query_string) +def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet: + bookmarks_query = query_shared_bookmarks(user, profile, query_string) query_set = Tag.objects.filter(bookmark__in=bookmarks_query) return query_set.distinct() -def query_shared_bookmark_users(query_string: str) -> QuerySet: - bookmarks_query = query_shared_bookmarks(None, query_string) +def query_shared_bookmark_users(profile: UserProfile, query_string: str) -> QuerySet: + bookmarks_query = query_shared_bookmarks(None, profile, query_string) query_set = User.objects.filter(bookmark__in=bookmarks_query) diff --git a/bookmarks/templates/bookmarks/bookmark_list.html b/bookmarks/templates/bookmarks/bookmark_list.html index 1efdf3b..eeb3782 100644 --- a/bookmarks/templates/bookmarks/bookmark_list.html +++ b/bookmarks/templates/bookmarks/bookmark_list.html @@ -22,7 +22,7 @@ {% if bookmark.tag_names %} {% for tag_name in bookmark.tag_names %} - {{ tag_name|hash_tag }} + {{ tag_name|hash_tag }} {% endfor %} {% endif %} diff --git a/bookmarks/templates/bookmarks/tag_cloud.html b/bookmarks/templates/bookmarks/tag_cloud.html index 921f0a9..02b44a8 100644 --- a/bookmarks/templates/bookmarks/tag_cloud.html +++ b/bookmarks/templates/bookmarks/tag_cloud.html @@ -4,7 +4,7 @@ {% if has_selected_tags %}

{% for tag in selected_tags %} - -{{ tag.name }} @@ -17,14 +17,14 @@ {% for tag in group.tags %} {# Highlight first char of first tag in group #} {% if forloop.counter == 1 %} - {{ tag.name|first_char }}{{ tag.name|remaining_chars:1 }} {% else %} {# Render remaining tags normally #} - {{ tag.name }} diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html index 94c58a5..8562127 100644 --- a/bookmarks/templates/settings/general.html +++ b/bookmarks/templates/settings/general.html @@ -36,6 +36,15 @@ Whether to open bookmarks a new page or in the same page. +

+ + {{ form.tag_search|add_class:"form-select col-2 col-sm-12" }} +
+ 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. +
+