mirror of
				https://github.com/sissbruecker/linkding.git
				synced 2025-11-03 20:44:05 +01:00 
			
		
		
		
	Allow saving search preferences (#540)
* Add indicator for modified filters * Rename shared filter values * Add update search preferences handler * Separate search and preferences forms * Properly initialize bookmark search from get or post * Add tests for applying search preferences * Implement saving search preferences * Remove bookmark search query alias * Use search preferences as default * Only show save button for authenticated users * Only show modified indicator if preferences are modified * Fix overriding search preferences * Add missing migration
This commit is contained in:
		@@ -34,7 +34,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
 | 
			
		||||
        user = self.request.user
 | 
			
		||||
        # For list action, use query set that applies search and tag projections
 | 
			
		||||
        if self.action == 'list':
 | 
			
		||||
            search = BookmarkSearch.from_request(self.request)
 | 
			
		||||
            search = BookmarkSearch.from_request(self.request.GET)
 | 
			
		||||
            return queries.query_bookmarks(user, user.profile, search)
 | 
			
		||||
 | 
			
		||||
        # For single entity actions use default query set without projections
 | 
			
		||||
@@ -46,7 +46,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
 | 
			
		||||
    @action(methods=['get'], detail=False)
 | 
			
		||||
    def archived(self, request):
 | 
			
		||||
        user = request.user
 | 
			
		||||
        search = BookmarkSearch.from_request(request)
 | 
			
		||||
        search = BookmarkSearch.from_request(request.GET)
 | 
			
		||||
        query_set = queries.query_archived_bookmarks(user, user.profile, search)
 | 
			
		||||
        page = self.paginate_queryset(query_set)
 | 
			
		||||
        serializer = self.get_serializer_class()
 | 
			
		||||
@@ -55,7 +55,7 @@ class BookmarkViewSet(viewsets.GenericViewSet,
 | 
			
		||||
 | 
			
		||||
    @action(methods=['get'], detail=False)
 | 
			
		||||
    def shared(self, request):
 | 
			
		||||
        search = BookmarkSearch.from_request(request)
 | 
			
		||||
        search = BookmarkSearch.from_request(request.GET)
 | 
			
		||||
        user = User.objects.filter(username=search.user).first()
 | 
			
		||||
        public_only = not request.user.is_authenticated
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(user, request.user_profile, search, public_only)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ class FeedContext:
 | 
			
		||||
class BaseBookmarksFeed(Feed):
 | 
			
		||||
    def get_object(self, request, feed_key: str):
 | 
			
		||||
        feed_token = FeedToken.objects.get(key__exact=feed_key)
 | 
			
		||||
        search = BookmarkSearch(query=request.GET.get('q', ''))
 | 
			
		||||
        search = BookmarkSearch(q=request.GET.get('q', ''))
 | 
			
		||||
        query_set = queries.query_bookmarks(feed_token.user, feed_token.user.profile, search)
 | 
			
		||||
        return FeedContext(feed_token, query_set)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								bookmarks/migrations/0025_userprofile_search_preferences.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								bookmarks/migrations/0025_userprofile_search_preferences.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 4.1.9 on 2023-09-30 10:44
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('bookmarks', '0024_userprofile_enable_public_sharing'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='userprofile',
 | 
			
		||||
            name='search_preferences',
 | 
			
		||||
            field=models.JSONField(default=dict),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -5,10 +5,10 @@ from typing import List
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.auth import get_user_model
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.core.handlers.wsgi import WSGIRequest
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.signals import post_save
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.http import QueryDict
 | 
			
		||||
 | 
			
		||||
from bookmarks.utils import unique
 | 
			
		||||
from bookmarks.validators import BookmarkURLValidator
 | 
			
		||||
@@ -130,15 +130,16 @@ class BookmarkSearch:
 | 
			
		||||
    SORT_TITLE_ASC = 'title_asc'
 | 
			
		||||
    SORT_TITLE_DESC = 'title_desc'
 | 
			
		||||
 | 
			
		||||
    FILTER_SHARED_OFF = ''
 | 
			
		||||
    FILTER_SHARED_SHARED = 'shared'
 | 
			
		||||
    FILTER_SHARED_UNSHARED = 'unshared'
 | 
			
		||||
    FILTER_SHARED_OFF = 'off'
 | 
			
		||||
    FILTER_SHARED_SHARED = 'yes'
 | 
			
		||||
    FILTER_SHARED_UNSHARED = 'no'
 | 
			
		||||
 | 
			
		||||
    FILTER_UNREAD_OFF = ''
 | 
			
		||||
    FILTER_UNREAD_OFF = 'off'
 | 
			
		||||
    FILTER_UNREAD_YES = 'yes'
 | 
			
		||||
    FILTER_UNREAD_NO = 'no'
 | 
			
		||||
 | 
			
		||||
    params = ['q', 'user', 'sort', 'shared', 'unread']
 | 
			
		||||
    preferences = ['sort', 'shared', 'unread']
 | 
			
		||||
    defaults = {
 | 
			
		||||
        'q': '',
 | 
			
		||||
        'user': '',
 | 
			
		||||
@@ -148,43 +149,59 @@ class BookmarkSearch:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def __init__(self,
 | 
			
		||||
                 q: str = defaults['q'],
 | 
			
		||||
                 query: str = defaults['q'],  # alias for q
 | 
			
		||||
                 user: str = defaults['user'],
 | 
			
		||||
                 sort: str = defaults['sort'],
 | 
			
		||||
                 shared: str = defaults['shared'],
 | 
			
		||||
                 unread: str = defaults['unread']):
 | 
			
		||||
        self.q = q or query
 | 
			
		||||
        self.user = user
 | 
			
		||||
        self.sort = sort
 | 
			
		||||
        self.shared = shared
 | 
			
		||||
        self.unread = unread
 | 
			
		||||
                 q: str = None,
 | 
			
		||||
                 user: str = None,
 | 
			
		||||
                 sort: str = None,
 | 
			
		||||
                 shared: str = None,
 | 
			
		||||
                 unread: str = None,
 | 
			
		||||
                 preferences: dict = None):
 | 
			
		||||
        if not preferences:
 | 
			
		||||
            preferences = {}
 | 
			
		||||
        self.defaults = {**BookmarkSearch.defaults, **preferences}
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def query(self):
 | 
			
		||||
        return self.q
 | 
			
		||||
        self.q = q or self.defaults['q']
 | 
			
		||||
        self.user = user or self.defaults['user']
 | 
			
		||||
        self.sort = sort or self.defaults['sort']
 | 
			
		||||
        self.shared = shared or self.defaults['shared']
 | 
			
		||||
        self.unread = unread or self.defaults['unread']
 | 
			
		||||
 | 
			
		||||
    def is_modified(self, param):
 | 
			
		||||
        value = self.__dict__[param]
 | 
			
		||||
        return value and value != BookmarkSearch.defaults[param]
 | 
			
		||||
        return value != self.defaults[param]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def modified_params(self):
 | 
			
		||||
        return [field for field in self.params if self.is_modified(field)]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def modified_preferences(self):
 | 
			
		||||
        return [preference for preference in self.preferences if self.is_modified(preference)]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_modifications(self):
 | 
			
		||||
        return len(self.modified_params) > 0
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_modified_preferences(self):
 | 
			
		||||
        return len(self.modified_preferences) > 0
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def query_params(self):
 | 
			
		||||
        return {param: self.__dict__[param] for param in self.modified_params}
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def preferences_dict(self):
 | 
			
		||||
        return {preference: self.__dict__[preference] for preference in self.preferences}
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def from_request(request: WSGIRequest):
 | 
			
		||||
    def from_request(query_dict: QueryDict, preferences: dict = None):
 | 
			
		||||
        initial_values = {}
 | 
			
		||||
        for param in BookmarkSearch.params:
 | 
			
		||||
            value = request.GET.get(param)
 | 
			
		||||
            value = query_dict.get(param)
 | 
			
		||||
            if value:
 | 
			
		||||
                initial_values[param] = value
 | 
			
		||||
 | 
			
		||||
        return BookmarkSearch(**initial_values)
 | 
			
		||||
        return BookmarkSearch(**initial_values, preferences=preferences)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BookmarkSearchForm(forms.Form):
 | 
			
		||||
@@ -214,6 +231,7 @@ class BookmarkSearchForm(forms.Form):
 | 
			
		||||
    def __init__(self, search: BookmarkSearch, editable_fields: List[str] = None, users: List[User] = None):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        editable_fields = editable_fields or []
 | 
			
		||||
        self.editable_fields = editable_fields
 | 
			
		||||
 | 
			
		||||
        # set choices for user field if users are provided
 | 
			
		||||
        if users:
 | 
			
		||||
@@ -282,6 +300,7 @@ class UserProfile(models.Model):
 | 
			
		||||
    enable_favicons = models.BooleanField(default=False, null=False)
 | 
			
		||||
    display_url = models.BooleanField(default=False, null=False)
 | 
			
		||||
    permanent_notes = models.BooleanField(default=False, null=False)
 | 
			
		||||
    search_preferences = models.JSONField(default=dict, null=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserProfileForm(forms.ModelForm):
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ def _base_bookmarks_query(user: Optional[User], profile: UserProfile, search: Bo
 | 
			
		||||
        query_set = query_set.filter(owner=user)
 | 
			
		||||
 | 
			
		||||
    # Split query into search terms and tags
 | 
			
		||||
    query = parse_query_string(search.query)
 | 
			
		||||
    query = parse_query_string(search.q)
 | 
			
		||||
 | 
			
		||||
    # Filter for search terms and tags
 | 
			
		||||
    for term in query['search_terms']:
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bookmarks-page #search {
 | 
			
		||||
.bookmarks-page .search-container {
 | 
			
		||||
  flex: 1 1 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@
 | 
			
		||||
@import "../../node_modules/spectre.css/src/media";
 | 
			
		||||
 | 
			
		||||
// Components
 | 
			
		||||
@import "../../node_modules/spectre.css/src/badges";
 | 
			
		||||
@import "../../node_modules/spectre.css/src/dropdowns";
 | 
			
		||||
@import "../../node_modules/spectre.css/src/empty";
 | 
			
		||||
@import "../../node_modules/spectre.css/src/menus";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,16 @@
 | 
			
		||||
{% load widget_tweaks %}
 | 
			
		||||
 | 
			
		||||
<form id="search" action="" method="get" role="search">
 | 
			
		||||
  <div class="input-group">
 | 
			
		||||
<div class="search-container">
 | 
			
		||||
  <form id="search" class="input-group" action="" method="get" role="search">
 | 
			
		||||
    <input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
 | 
			
		||||
           value="{{ search.query }}">
 | 
			
		||||
           value="{{ search.q }}">
 | 
			
		||||
    <input type="submit" value="Search" class="btn input-group-btn">
 | 
			
		||||
  </div>
 | 
			
		||||
    {% for hidden_field in search_form.hidden_fields %}
 | 
			
		||||
      {{ hidden_field }}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
  </form>
 | 
			
		||||
  <div class="search-options dropdown dropdown-right">
 | 
			
		||||
    <button type="button" class="btn dropdown-toggle">
 | 
			
		||||
    <button type="button" class="btn dropdown-toggle{% if search.has_modified_preferences %} badge{% endif %}">
 | 
			
		||||
      <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" stroke-width="2"
 | 
			
		||||
           stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
 | 
			
		||||
        <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
 | 
			
		||||
@@ -23,40 +26,54 @@
 | 
			
		||||
      </svg>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div class="menu text-sm" tabindex="0">
 | 
			
		||||
      <div class="form-group">
 | 
			
		||||
        <label for="{{ form.sort.id_for_label }}" class="form-label">Sort by</label>
 | 
			
		||||
        {{ form.sort|add_class:"form-select select-sm" }}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="form-group radio-group">
 | 
			
		||||
        <div class="form-label">Shared filter</div>
 | 
			
		||||
        {% for radio in form.shared %}
 | 
			
		||||
          <label for="{{ radio.id_for_label }}" class="form-radio form-inline">
 | 
			
		||||
            {{ radio.tag }}
 | 
			
		||||
            <i class="form-icon"></i>
 | 
			
		||||
            {{ radio.choice_label }}
 | 
			
		||||
          </label>
 | 
			
		||||
      <form id="search_preferences" action="" method="post">
 | 
			
		||||
        {% csrf_token %}
 | 
			
		||||
        {% if 'sort' in preferences_form.editable_fields %}
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label for="{{ preferences_form.sort.id_for_label }}"
 | 
			
		||||
                   class="form-label{% if 'sort' in search.modified_params %} text-bold{% endif %}">Sort by</label>
 | 
			
		||||
            {{ preferences_form.sort|add_class:"form-select select-sm" }}
 | 
			
		||||
          </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if 'shared' in preferences_form.editable_fields %}
 | 
			
		||||
          <div class="form-group radio-group">
 | 
			
		||||
            <div class="form-label{% if 'shared' in search.modified_params %} text-bold{% endif %}">Shared filter</div>
 | 
			
		||||
            {% for radio in preferences_form.shared %}
 | 
			
		||||
              <label for="{{ radio.id_for_label }}" class="form-radio form-inline">
 | 
			
		||||
                {{ radio.tag }}
 | 
			
		||||
                <i class="form-icon"></i>
 | 
			
		||||
                {{ radio.choice_label }}
 | 
			
		||||
              </label>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if 'unread' in preferences_form.editable_fields %}
 | 
			
		||||
          <div class="form-group radio-group">
 | 
			
		||||
            <div class="form-label{% if 'unread' in search.modified_params %} text-bold{% endif %}">Unread filter</div>
 | 
			
		||||
            {% for radio in preferences_form.unread %}
 | 
			
		||||
              <label for="{{ radio.id_for_label }}" class="form-radio form-inline">
 | 
			
		||||
                {{ radio.tag }}
 | 
			
		||||
                <i class="form-icon"></i>
 | 
			
		||||
                {{ radio.choice_label }}
 | 
			
		||||
              </label>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <div class="actions">
 | 
			
		||||
          <button type="submit" class="btn btn-sm btn-primary" name="apply">Apply</button>
 | 
			
		||||
          {% if request.user.is_authenticated %}
 | 
			
		||||
            <button type="submit" class="btn btn-sm" name="save">Save as default</button>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {% for hidden_field in preferences_form.hidden_fields %}
 | 
			
		||||
          {{ hidden_field }}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="form-group radio-group">
 | 
			
		||||
        <div class="form-label">Unread filter</div>
 | 
			
		||||
        {% for radio in form.unread %}
 | 
			
		||||
          <label for="{{ radio.id_for_label }}" class="form-radio form-inline">
 | 
			
		||||
            {{ radio.tag }}
 | 
			
		||||
            <i class="form-icon"></i>
 | 
			
		||||
            {{ radio.choice_label }}
 | 
			
		||||
          </label>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="actions">
 | 
			
		||||
        <button type="submit" class="btn btn-sm btn-primary">Apply</button>
 | 
			
		||||
      </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
  {% for hidden_field in form.hidden_fields %}
 | 
			
		||||
    {{ hidden_field }}
 | 
			
		||||
  {% endfor %}
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
{# Replace search input with auto-complete component #}
 | 
			
		||||
<script type="application/javascript">
 | 
			
		||||
@@ -65,7 +82,7 @@
 | 
			
		||||
    const currentTags = currentTagsString.split(' ');
 | 
			
		||||
    const uniqueTags = [...new Set(currentTags)]
 | 
			
		||||
    const search = {
 | 
			
		||||
      q: '{{ search.query }}',
 | 
			
		||||
      q: '{{ search.q }}',
 | 
			
		||||
      user: '{{ search.user }}',
 | 
			
		||||
      shared: '{{ search.shared }}',
 | 
			
		||||
      unread: '{{ search.unread }}',
 | 
			
		||||
@@ -78,7 +95,7 @@
 | 
			
		||||
      props: {
 | 
			
		||||
        name: 'q',
 | 
			
		||||
        placeholder: 'Search for words or #tags',
 | 
			
		||||
        value: '{{ search.query|safe }}',
 | 
			
		||||
        value: '{{ search.q|safe }}',
 | 
			
		||||
        tags: uniqueTags,
 | 
			
		||||
        mode: '{{ mode }}',
 | 
			
		||||
        linkTarget: '{{ request.user_profile.bookmark_link_target }}',
 | 
			
		||||
 
 | 
			
		||||
@@ -22,11 +22,17 @@ def bookmark_form(context, form: BookmarkForm, cancel_url: str, bookmark_id: int
 | 
			
		||||
def bookmark_search(context, search: BookmarkSearch, tags: [Tag], mode: str = ''):
 | 
			
		||||
    tag_names = [tag.name for tag in tags]
 | 
			
		||||
    tags_string = build_tag_string(tag_names, ' ')
 | 
			
		||||
    form = BookmarkSearchForm(search, editable_fields=['q', 'sort', 'shared', 'unread'])
 | 
			
		||||
    search_form = BookmarkSearchForm(search, editable_fields=['q'])
 | 
			
		||||
 | 
			
		||||
    if mode == 'shared':
 | 
			
		||||
        preferences_form = BookmarkSearchForm(search, editable_fields=['sort'])
 | 
			
		||||
    else:
 | 
			
		||||
        preferences_form = BookmarkSearchForm(search, editable_fields=['sort', 'shared', 'unread'])
 | 
			
		||||
    return {
 | 
			
		||||
        'request': context['request'],
 | 
			
		||||
        'search': search,
 | 
			
		||||
        'form': form,
 | 
			
		||||
        'search_form': search_form,
 | 
			
		||||
        'preferences_form': preferences_form,
 | 
			
		||||
        'tags_string': tags_string,
 | 
			
		||||
        'mode': mode,
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,7 @@ class BookmarkFactoryMixin:
 | 
			
		||||
            tags = []
 | 
			
		||||
            if with_tags:
 | 
			
		||||
                tag_name = f'{tag_prefix} {i}{suffix}'
 | 
			
		||||
                tags = [self.setup_tag(name=tag_name)]
 | 
			
		||||
                tags = [self.setup_tag(name=tag_name, user=user)]
 | 
			
		||||
            bookmark = self.setup_bookmark(url=url,
 | 
			
		||||
                                           title=title,
 | 
			
		||||
                                           is_archived=archived,
 | 
			
		||||
@@ -139,6 +139,12 @@ class BookmarkFactoryMixin:
 | 
			
		||||
        user.profile.save()
 | 
			
		||||
        return user
 | 
			
		||||
 | 
			
		||||
    def get_tags_from_bookmarks(self, bookmarks: [Bookmark]):
 | 
			
		||||
        all_tags = []
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
            all_tags = all_tags + list(bookmark.tags.all())
 | 
			
		||||
        return all_tags
 | 
			
		||||
 | 
			
		||||
    def get_random_string(self, length: int = 32):
 | 
			
		||||
        return get_random_string(length=length)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -440,21 +440,6 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
 | 
			
		||||
        self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
 | 
			
		||||
 | 
			
		||||
    def test_bulk_select_across_respects_query(self):
 | 
			
		||||
        self.setup_numbered_bookmarks(3, prefix='foo')
 | 
			
		||||
        self.setup_numbered_bookmarks(3, prefix='bar')
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(3, Bookmark.objects.filter(title__startswith='foo').count())
 | 
			
		||||
 | 
			
		||||
        self.client.post(reverse('bookmarks:index.action') + '?q=foo', {
 | 
			
		||||
            'bulk_action': ['bulk_delete'],
 | 
			
		||||
            'bulk_execute': [''],
 | 
			
		||||
            'bulk_select_across': ['on'],
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count())
 | 
			
		||||
        self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count())
 | 
			
		||||
 | 
			
		||||
    def test_bulk_select_across_ignores_page(self):
 | 
			
		||||
        self.setup_numbered_bookmarks(100)
 | 
			
		||||
 | 
			
		||||
@@ -493,6 +478,21 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertIsNone(Bookmark.objects.filter(title='Bookmark 2').first())
 | 
			
		||||
        self.assertIsNone(Bookmark.objects.filter(title='Bookmark 3').first())
 | 
			
		||||
 | 
			
		||||
    def test_index_action_bulk_select_across_respects_query(self):
 | 
			
		||||
        self.setup_numbered_bookmarks(3, prefix='foo')
 | 
			
		||||
        self.setup_numbered_bookmarks(3, prefix='bar')
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(3, Bookmark.objects.filter(title__startswith='foo').count())
 | 
			
		||||
 | 
			
		||||
        self.client.post(reverse('bookmarks:index.action') + '?q=foo', {
 | 
			
		||||
            'bulk_action': ['bulk_delete'],
 | 
			
		||||
            'bulk_execute': [''],
 | 
			
		||||
            'bulk_select_across': ['on'],
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count())
 | 
			
		||||
        self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count())
 | 
			
		||||
 | 
			
		||||
    def test_archived_action_bulk_select_across_only_affects_archived_bookmarks(self):
 | 
			
		||||
        self.setup_bulk_edit_scope_test_data()
 | 
			
		||||
 | 
			
		||||
@@ -511,6 +511,21 @@ class BookmarkActionViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 2').first())
 | 
			
		||||
        self.assertIsNone(Bookmark.objects.filter(title='Archived Bookmark 3').first())
 | 
			
		||||
 | 
			
		||||
    def test_archived_action_bulk_select_across_respects_query(self):
 | 
			
		||||
        self.setup_numbered_bookmarks(3, prefix='foo', archived=True)
 | 
			
		||||
        self.setup_numbered_bookmarks(3, prefix='bar', archived=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(3, Bookmark.objects.filter(title__startswith='foo').count())
 | 
			
		||||
 | 
			
		||||
        self.client.post(reverse('bookmarks:archived.action') + '?q=foo', {
 | 
			
		||||
            'bulk_action': ['bulk_delete'],
 | 
			
		||||
            'bulk_execute': [''],
 | 
			
		||||
            'bulk_select_across': ['on'],
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(0, Bookmark.objects.filter(title__startswith='foo').count())
 | 
			
		||||
        self.assertEqual(3, Bookmark.objects.filter(title__startswith='bar').count())
 | 
			
		||||
 | 
			
		||||
    def test_shared_action_bulk_select_across_not_supported(self):
 | 
			
		||||
        self.setup_bulk_edit_scope_test_data()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from bookmarks.models import Bookmark, Tag, UserProfile
 | 
			
		||||
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin, collapse_whitespace
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -16,38 +16,51 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
        self.client.force_login(user)
 | 
			
		||||
 | 
			
		||||
    def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
        self.assertContains(response, '<li ld-bookmark-item>', count=len(bookmarks))
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]')
 | 
			
		||||
        self.assertIsNotNone(bookmark_list)
 | 
			
		||||
 | 
			
		||||
        bookmark_items = bookmark_list.select('li[ld-bookmark-item]')
 | 
			
		||||
        self.assertEqual(len(bookmark_items), len(bookmarks))
 | 
			
		||||
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
            self.assertInHTML(
 | 
			
		||||
                f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
 | 
			
		||||
                html
 | 
			
		||||
            )
 | 
			
		||||
            bookmark_item = bookmark_list.select_one(
 | 
			
		||||
                f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
 | 
			
		||||
            self.assertIsNotNone(bookmark_item)
 | 
			
		||||
 | 
			
		||||
    def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
            self.assertInHTML(
 | 
			
		||||
                f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
 | 
			
		||||
                html,
 | 
			
		||||
                count=0
 | 
			
		||||
            )
 | 
			
		||||
            bookmark_item = soup.select_one(
 | 
			
		||||
                f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
 | 
			
		||||
            self.assertIsNone(bookmark_item)
 | 
			
		||||
 | 
			
		||||
    def assertVisibleTags(self, response, tags: List[Tag]):
 | 
			
		||||
        self.assertContains(response, 'data-is-tag-item', count=len(tags))
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        tag_cloud = soup.select_one('div.tag-cloud')
 | 
			
		||||
        self.assertIsNotNone(tag_cloud)
 | 
			
		||||
 | 
			
		||||
        tag_items = tag_cloud.select('a[data-is-tag-item]')
 | 
			
		||||
        self.assertEqual(len(tag_items), len(tags))
 | 
			
		||||
 | 
			
		||||
        tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
 | 
			
		||||
 | 
			
		||||
        for tag in tags:
 | 
			
		||||
            self.assertContains(response, tag.name)
 | 
			
		||||
            self.assertTrue(tag.name in tag_item_names)
 | 
			
		||||
 | 
			
		||||
    def assertInvisibleTags(self, response, tags: List[Tag]):
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        tag_items = soup.select('a[data-is-tag-item]')
 | 
			
		||||
 | 
			
		||||
        tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
 | 
			
		||||
 | 
			
		||||
        for tag in tags:
 | 
			
		||||
            self.assertNotContains(response, tag.name)
 | 
			
		||||
            self.assertFalse(tag.name in tag_item_names)
 | 
			
		||||
 | 
			
		||||
    def assertSelectedTags(self, response, tags: List[Tag]):
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        selected_tags = soup.select('p.selected-tags')[0]
 | 
			
		||||
        selected_tags = soup.select_one('p.selected-tags')
 | 
			
		||||
        self.assertIsNotNone(selected_tags)
 | 
			
		||||
 | 
			
		||||
        tag_list = selected_tags.select('a')
 | 
			
		||||
@@ -73,67 +86,36 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
 | 
			
		||||
    def test_should_list_archived_and_user_owned_bookmarks(self):
 | 
			
		||||
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(is_archived=True)
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
 | 
			
		||||
        invisible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(is_archived=False),
 | 
			
		||||
            self.setup_bookmark(is_archived=True, user=other_user),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived'))
 | 
			
		||||
        html = collapse_whitespace(response.content.decode())
 | 
			
		||||
 | 
			
		||||
        # Should render list
 | 
			
		||||
        self.assertIn('<ul class="bookmark-list" data-bookmarks-total="3">', html)
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_bookmarks_matching_query(self):
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(is_archived=True, title='searchvalue'),
 | 
			
		||||
            self.setup_bookmark(is_archived=True, title='searchvalue'),
 | 
			
		||||
            self.setup_bookmark(is_archived=True, title='searchvalue')
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(is_archived=True)
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, prefix='foo', archived=True)
 | 
			
		||||
        invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix='bar', archived=True)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived') + '?q=searchvalue')
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived') + '?q=foo')
 | 
			
		||||
        html = collapse_whitespace(response.content.decode())
 | 
			
		||||
 | 
			
		||||
        # Should render list
 | 
			
		||||
        self.assertIn('<ul class="bookmark-list" data-bookmarks-total="3">', html)
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_tags_for_archived_and_user_owned_bookmarks(self):
 | 
			
		||||
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
 | 
			
		||||
        visible_tags = [
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_tags = [
 | 
			
		||||
            self.setup_tag(),  # unused tag
 | 
			
		||||
            self.setup_tag(),  # used in archived bookmark
 | 
			
		||||
            self.setup_tag(user=other_user),  # belongs to other user
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True)
 | 
			
		||||
        unarchived_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=False, tag_prefix='unarchived')
 | 
			
		||||
        other_user_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, user=other_user,
 | 
			
		||||
                                                             tag_prefix='otheruser')
 | 
			
		||||
 | 
			
		||||
        # Assign tags to some bookmarks with duplicates
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[0]])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[0]])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[1]])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[1]])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[2]])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[2]])
 | 
			
		||||
 | 
			
		||||
        self.setup_bookmark(is_archived=False, tags=[invisible_tags[1]])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[invisible_tags[2]], user=other_user)
 | 
			
		||||
        visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
 | 
			
		||||
        invisible_tags = self.get_tags_from_bookmarks(unarchived_bookmarks + other_user_bookmarks)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived'))
 | 
			
		||||
 | 
			
		||||
@@ -141,29 +123,40 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
        self.assertInvisibleTags(response, invisible_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_tags_for_bookmarks_matching_query(self):
 | 
			
		||||
        visible_tags = [
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_tags = [
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, prefix='foo',
 | 
			
		||||
                                                          tag_prefix='foo')
 | 
			
		||||
        invisible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, prefix='bar',
 | 
			
		||||
                                                            tag_prefix='bar')
 | 
			
		||||
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[0]], title='searchvalue')
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[1]], title='searchvalue')
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[visible_tags[2]], title='searchvalue')
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[invisible_tags[0]])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[invisible_tags[1]])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[invisible_tags[2]])
 | 
			
		||||
        visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
 | 
			
		||||
        invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived') + '?q=searchvalue')
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived') + '?q=foo')
 | 
			
		||||
 | 
			
		||||
        self.assertVisibleTags(response, visible_tags)
 | 
			
		||||
        self.assertInvisibleTags(response, invisible_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_bookmarks_and_tags_for_search_preferences(self):
 | 
			
		||||
        user_profile = self.user.profile
 | 
			
		||||
        user_profile.search_preferences = {
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        user_profile.save()
 | 
			
		||||
 | 
			
		||||
        unread_bookmarks = self.setup_numbered_bookmarks(3, archived=True, unread=True, with_tags=True, prefix='unread',
 | 
			
		||||
                                                         tag_prefix='unread')
 | 
			
		||||
        read_bookmarks = self.setup_numbered_bookmarks(3, archived=True, unread=False, with_tags=True, prefix='read',
 | 
			
		||||
                                                       tag_prefix='read')
 | 
			
		||||
 | 
			
		||||
        unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
 | 
			
		||||
        read_tags = self.get_tags_from_bookmarks(read_bookmarks)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived'))
 | 
			
		||||
        self.assertVisibleBookmarks(response, unread_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, read_bookmarks)
 | 
			
		||||
        self.assertVisibleTags(response, unread_tags)
 | 
			
		||||
        self.assertInvisibleTags(response, read_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_display_selected_tags_from_query(self):
 | 
			
		||||
        tags = [
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
@@ -210,11 +203,7 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
        self.assertSelectedTags(response, [tags[0], tags[1]])
 | 
			
		||||
 | 
			
		||||
    def test_should_open_bookmarks_in_new_page_by_default(self):
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(is_archived=True)
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived'))
 | 
			
		||||
 | 
			
		||||
@@ -225,11 +214,7 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
        user.profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
 | 
			
		||||
        user.profile.save()
 | 
			
		||||
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(is_archived=True)
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, archived=True)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:archived'))
 | 
			
		||||
 | 
			
		||||
@@ -328,6 +313,106 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
          </select>
 | 
			
		||||
        ''', html)
 | 
			
		||||
 | 
			
		||||
    def test_apply_search_preferences(self):
 | 
			
		||||
        # no params
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:archived'))
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:archived'))
 | 
			
		||||
 | 
			
		||||
        # some params
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:archived'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&sort=title_asc')
 | 
			
		||||
 | 
			
		||||
        # params with default value are removed
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:archived'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'user': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&unread=yes')
 | 
			
		||||
 | 
			
		||||
        # page is removed
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:archived'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'page': '2',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:archived') + '?q=foo&sort=title_asc')
 | 
			
		||||
 | 
			
		||||
    def test_save_search_preferences(self):
 | 
			
		||||
        user_profile = self.user.profile
 | 
			
		||||
 | 
			
		||||
        # no params
 | 
			
		||||
        self.client.post(reverse('bookmarks:archived'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # with param
 | 
			
		||||
        self.client.post(reverse('bookmarks:archived'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # add a param
 | 
			
		||||
        self.client.post(reverse('bookmarks:archived'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # remove a param
 | 
			
		||||
        self.client.post(reverse('bookmarks:archived'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # ignores non-preferences
 | 
			
		||||
        self.client.post(reverse('bookmarks:archived'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'user': 'john',
 | 
			
		||||
            'page': '3',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_url_encode_bookmark_actions_url(self):
 | 
			
		||||
        url = reverse('bookmarks:archived') + '?q=%23foo'
 | 
			
		||||
        response = self.client.get(url)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from bookmarks.models import Bookmark, Tag, UserProfile
 | 
			
		||||
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin, collapse_whitespace
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -16,38 +16,51 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        self.client.force_login(user)
 | 
			
		||||
 | 
			
		||||
    def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
        self.assertContains(response, '<li ld-bookmark-item>', count=len(bookmarks))
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]')
 | 
			
		||||
        self.assertIsNotNone(bookmark_list)
 | 
			
		||||
 | 
			
		||||
        bookmark_items = bookmark_list.select('li[ld-bookmark-item]')
 | 
			
		||||
        self.assertEqual(len(bookmark_items), len(bookmarks))
 | 
			
		||||
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
            self.assertInHTML(
 | 
			
		||||
                f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
 | 
			
		||||
                html
 | 
			
		||||
            )
 | 
			
		||||
            bookmark_item = bookmark_list.select_one(
 | 
			
		||||
                f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
 | 
			
		||||
            self.assertIsNotNone(bookmark_item)
 | 
			
		||||
 | 
			
		||||
    def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
            self.assertInHTML(
 | 
			
		||||
                f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
 | 
			
		||||
                html,
 | 
			
		||||
                count=0
 | 
			
		||||
            )
 | 
			
		||||
            bookmark_item = soup.select_one(
 | 
			
		||||
                f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
 | 
			
		||||
            self.assertIsNone(bookmark_item)
 | 
			
		||||
 | 
			
		||||
    def assertVisibleTags(self, response, tags: List[Tag]):
 | 
			
		||||
        self.assertContains(response, 'data-is-tag-item', count=len(tags))
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        tag_cloud = soup.select_one('div.tag-cloud')
 | 
			
		||||
        self.assertIsNotNone(tag_cloud)
 | 
			
		||||
 | 
			
		||||
        tag_items = tag_cloud.select('a[data-is-tag-item]')
 | 
			
		||||
        self.assertEqual(len(tag_items), len(tags))
 | 
			
		||||
 | 
			
		||||
        tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
 | 
			
		||||
 | 
			
		||||
        for tag in tags:
 | 
			
		||||
            self.assertContains(response, tag.name)
 | 
			
		||||
            self.assertTrue(tag.name in tag_item_names)
 | 
			
		||||
 | 
			
		||||
    def assertInvisibleTags(self, response, tags: List[Tag]):
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        tag_items = soup.select('a[data-is-tag-item]')
 | 
			
		||||
 | 
			
		||||
        tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
 | 
			
		||||
 | 
			
		||||
        for tag in tags:
 | 
			
		||||
            self.assertNotContains(response, tag.name)
 | 
			
		||||
            self.assertFalse(tag.name in tag_item_names)
 | 
			
		||||
 | 
			
		||||
    def assertSelectedTags(self, response, tags: List[Tag]):
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        selected_tags = soup.select('p.selected-tags')[0]
 | 
			
		||||
        selected_tags = soup.select_one('p.selected-tags')
 | 
			
		||||
        self.assertIsNotNone(selected_tags)
 | 
			
		||||
 | 
			
		||||
        tag_list = selected_tags.select('a')
 | 
			
		||||
@@ -73,67 +86,34 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
 | 
			
		||||
    def test_should_list_unarchived_and_user_owned_bookmarks(self):
 | 
			
		||||
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(),
 | 
			
		||||
            self.setup_bookmark(),
 | 
			
		||||
            self.setup_bookmark()
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3)
 | 
			
		||||
        invisible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(is_archived=True),
 | 
			
		||||
            self.setup_bookmark(user=other_user),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index'))
 | 
			
		||||
        html = collapse_whitespace(response.content.decode())
 | 
			
		||||
 | 
			
		||||
        # Should render list
 | 
			
		||||
        self.assertIn('<ul class="bookmark-list" data-bookmarks-total="3">', html)
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_bookmarks_matching_query(self):
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(title='searchvalue'),
 | 
			
		||||
            self.setup_bookmark(title='searchvalue'),
 | 
			
		||||
            self.setup_bookmark(title='searchvalue')
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(),
 | 
			
		||||
            self.setup_bookmark(),
 | 
			
		||||
            self.setup_bookmark()
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, prefix='foo')
 | 
			
		||||
        invisible_bookmarks = self.setup_numbered_bookmarks(3, prefix='bar')
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index') + '?q=searchvalue')
 | 
			
		||||
        html = collapse_whitespace(response.content.decode())
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index') + '?q=foo')
 | 
			
		||||
 | 
			
		||||
        # Should render list
 | 
			
		||||
        self.assertIn('<ul class="bookmark-list" data-bookmarks-total="3">', html)
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_tags_for_unarchived_and_user_owned_bookmarks(self):
 | 
			
		||||
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
 | 
			
		||||
        visible_tags = [
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_tags = [
 | 
			
		||||
            self.setup_tag(),  # unused tag
 | 
			
		||||
            self.setup_tag(),  # used in archived bookmark
 | 
			
		||||
            self.setup_tag(user=other_user),  # belongs to other user
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True)
 | 
			
		||||
        archived_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, archived=True, tag_prefix='archived')
 | 
			
		||||
        other_user_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, user=other_user, tag_prefix='otheruser')
 | 
			
		||||
 | 
			
		||||
        # Assign tags to some bookmarks with duplicates
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[0]])
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[0]])
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[1]])
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[1]])
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[2]])
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[2]])
 | 
			
		||||
 | 
			
		||||
        self.setup_bookmark(tags=[invisible_tags[1]], is_archived=True)
 | 
			
		||||
        self.setup_bookmark(tags=[invisible_tags[2]], user=other_user)
 | 
			
		||||
        visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
 | 
			
		||||
        invisible_tags = self.get_tags_from_bookmarks(archived_bookmarks + other_user_bookmarks)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index'))
 | 
			
		||||
 | 
			
		||||
@@ -141,29 +121,38 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        self.assertInvisibleTags(response, invisible_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_tags_for_bookmarks_matching_query(self):
 | 
			
		||||
        visible_tags = [
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_tags = [
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, prefix='foo', tag_prefix='foo')
 | 
			
		||||
        invisible_bookmarks = self.setup_numbered_bookmarks(3, with_tags=True, prefix='bar', tag_prefix='bar')
 | 
			
		||||
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[0]], title='searchvalue')
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[1]], title='searchvalue')
 | 
			
		||||
        self.setup_bookmark(tags=[visible_tags[2]], title='searchvalue')
 | 
			
		||||
        self.setup_bookmark(tags=[invisible_tags[0]])
 | 
			
		||||
        self.setup_bookmark(tags=[invisible_tags[1]])
 | 
			
		||||
        self.setup_bookmark(tags=[invisible_tags[2]])
 | 
			
		||||
        visible_tags = self.get_tags_from_bookmarks(visible_bookmarks)
 | 
			
		||||
        invisible_tags = self.get_tags_from_bookmarks(invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index') + '?q=searchvalue')
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index') + '?q=foo')
 | 
			
		||||
 | 
			
		||||
        self.assertVisibleTags(response, visible_tags)
 | 
			
		||||
        self.assertInvisibleTags(response, invisible_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_bookmarks_and_tags_for_search_preferences(self):
 | 
			
		||||
        user_profile = self.user.profile
 | 
			
		||||
        user_profile.search_preferences = {
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        user_profile.save()
 | 
			
		||||
 | 
			
		||||
        unread_bookmarks = self.setup_numbered_bookmarks(3, unread=True, with_tags=True, prefix='unread',
 | 
			
		||||
                                                         tag_prefix='unread')
 | 
			
		||||
        read_bookmarks = self.setup_numbered_bookmarks(3, unread=False, with_tags=True, prefix='read',
 | 
			
		||||
                                                       tag_prefix='read')
 | 
			
		||||
 | 
			
		||||
        unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
 | 
			
		||||
        read_tags = self.get_tags_from_bookmarks(read_bookmarks)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index'))
 | 
			
		||||
        self.assertVisibleBookmarks(response, unread_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, read_bookmarks)
 | 
			
		||||
        self.assertVisibleTags(response, unread_tags)
 | 
			
		||||
        self.assertInvisibleTags(response, read_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_display_selected_tags_from_query(self):
 | 
			
		||||
        tags = [
 | 
			
		||||
            self.setup_tag(),
 | 
			
		||||
@@ -210,11 +199,7 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        self.assertSelectedTags(response, [tags[0], tags[1]])
 | 
			
		||||
 | 
			
		||||
    def test_should_open_bookmarks_in_new_page_by_default(self):
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(),
 | 
			
		||||
            self.setup_bookmark(),
 | 
			
		||||
            self.setup_bookmark()
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index'))
 | 
			
		||||
 | 
			
		||||
@@ -225,11 +210,7 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        user.profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
 | 
			
		||||
        user.profile.save()
 | 
			
		||||
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(),
 | 
			
		||||
            self.setup_bookmark(),
 | 
			
		||||
            self.setup_bookmark()
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:index'))
 | 
			
		||||
 | 
			
		||||
@@ -328,6 +309,106 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
          </select>
 | 
			
		||||
        ''', html)
 | 
			
		||||
 | 
			
		||||
    def test_apply_search_preferences(self):
 | 
			
		||||
        # no params
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:index'))
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:index'))
 | 
			
		||||
 | 
			
		||||
        # some params
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:index'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&sort=title_asc')
 | 
			
		||||
 | 
			
		||||
        # params with default value are removed
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:index'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'user': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&unread=yes')
 | 
			
		||||
 | 
			
		||||
        # page is removed
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:index'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'page': '2',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:index') + '?q=foo&sort=title_asc')
 | 
			
		||||
 | 
			
		||||
    def test_save_search_preferences(self):
 | 
			
		||||
        user_profile = self.user.profile
 | 
			
		||||
 | 
			
		||||
        # no params
 | 
			
		||||
        self.client.post(reverse('bookmarks:index'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # with param
 | 
			
		||||
        self.client.post(reverse('bookmarks:index'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # add a param
 | 
			
		||||
        self.client.post(reverse('bookmarks:index'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # remove a param
 | 
			
		||||
        self.client.post(reverse('bookmarks:index'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # ignores non-preferences
 | 
			
		||||
        self.client.post(reverse('bookmarks:index'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'user': 'john',
 | 
			
		||||
            'page': '3',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_url_encode_bookmark_actions_url(self):
 | 
			
		||||
        url = reverse('bookmarks:index') + '?q=%23foo'
 | 
			
		||||
        response = self.client.get(url)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,10 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        search = BookmarkSearch()
 | 
			
		||||
        form = BookmarkSearchForm(search)
 | 
			
		||||
        self.assertEqual(form['q'].initial, '')
 | 
			
		||||
        self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_DESC)
 | 
			
		||||
        self.assertEqual(form['user'].initial, '')
 | 
			
		||||
        self.assertEqual(form['shared'].initial, '')
 | 
			
		||||
        self.assertEqual(form['unread'].initial, '')
 | 
			
		||||
        self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_DESC)
 | 
			
		||||
        self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_OFF)
 | 
			
		||||
        self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_OFF)
 | 
			
		||||
 | 
			
		||||
        # with params
 | 
			
		||||
        search = BookmarkSearch(q='search query',
 | 
			
		||||
@@ -23,8 +23,8 @@ class BookmarkSearchFormTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
                                unread=BookmarkSearch.FILTER_UNREAD_YES)
 | 
			
		||||
        form = BookmarkSearchForm(search)
 | 
			
		||||
        self.assertEqual(form['q'].initial, 'search query')
 | 
			
		||||
        self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_ASC)
 | 
			
		||||
        self.assertEqual(form['user'].initial, 'user123')
 | 
			
		||||
        self.assertEqual(form['sort'].initial, BookmarkSearch.SORT_ADDED_ASC)
 | 
			
		||||
        self.assertEqual(form['shared'].initial, BookmarkSearch.FILTER_SHARED_SHARED)
 | 
			
		||||
        self.assertEqual(form['unread'].initial, BookmarkSearch.FILTER_UNREAD_YES)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,50 +1,70 @@
 | 
			
		||||
from unittest.mock import Mock
 | 
			
		||||
from bookmarks.models import BookmarkSearch
 | 
			
		||||
from django.http import QueryDict
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from bookmarks.models import BookmarkSearch
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BookmarkSearchModelTest(TestCase):
 | 
			
		||||
    def test_from_request(self):
 | 
			
		||||
        # no params
 | 
			
		||||
        mock_request = Mock()
 | 
			
		||||
        mock_request.GET = {}
 | 
			
		||||
        query_dict = QueryDict()
 | 
			
		||||
 | 
			
		||||
        search = BookmarkSearch.from_request(mock_request)
 | 
			
		||||
        search = BookmarkSearch.from_request(query_dict)
 | 
			
		||||
        self.assertEqual(search.q, '')
 | 
			
		||||
        self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
 | 
			
		||||
        self.assertEqual(search.user, '')
 | 
			
		||||
        self.assertEqual(search.shared, '')
 | 
			
		||||
        self.assertEqual(search.unread, '')
 | 
			
		||||
        self.assertEqual(search.sort, BookmarkSearch.SORT_ADDED_DESC)
 | 
			
		||||
        self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
 | 
			
		||||
        self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
 | 
			
		||||
 | 
			
		||||
        # some params
 | 
			
		||||
        mock_request.GET = {
 | 
			
		||||
            'q': 'search query',
 | 
			
		||||
            'user': 'user123',
 | 
			
		||||
        }
 | 
			
		||||
        query_dict = QueryDict('q=search query&user=user123')
 | 
			
		||||
 | 
			
		||||
        bookmark_search = BookmarkSearch.from_request(mock_request)
 | 
			
		||||
        bookmark_search = BookmarkSearch.from_request(query_dict)
 | 
			
		||||
        self.assertEqual(bookmark_search.q, 'search query')
 | 
			
		||||
        self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
 | 
			
		||||
        self.assertEqual(bookmark_search.user, 'user123')
 | 
			
		||||
        self.assertEqual(bookmark_search.shared, '')
 | 
			
		||||
        self.assertEqual(bookmark_search.unread, '')
 | 
			
		||||
        self.assertEqual(bookmark_search.sort, BookmarkSearch.SORT_ADDED_DESC)
 | 
			
		||||
        self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
 | 
			
		||||
        self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
 | 
			
		||||
 | 
			
		||||
        # all params
 | 
			
		||||
        mock_request.GET = {
 | 
			
		||||
            'q': 'search query',
 | 
			
		||||
            'user': 'user123',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_SHARED,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        query_dict = QueryDict('q=search query&sort=title_asc&user=user123&shared=yes&unread=yes')
 | 
			
		||||
 | 
			
		||||
        search = BookmarkSearch.from_request(mock_request)
 | 
			
		||||
        search = BookmarkSearch.from_request(query_dict)
 | 
			
		||||
        self.assertEqual(search.q, 'search query')
 | 
			
		||||
        self.assertEqual(search.user, 'user123')
 | 
			
		||||
        self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
 | 
			
		||||
        self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_SHARED)
 | 
			
		||||
        self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
 | 
			
		||||
 | 
			
		||||
        # respects preferences
 | 
			
		||||
        preferences = {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        query_dict = QueryDict('q=search query')
 | 
			
		||||
 | 
			
		||||
        search = BookmarkSearch.from_request(query_dict, preferences)
 | 
			
		||||
        self.assertEqual(search.q, 'search query')
 | 
			
		||||
        self.assertEqual(search.user, '')
 | 
			
		||||
        self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_ASC)
 | 
			
		||||
        self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_OFF)
 | 
			
		||||
        self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_YES)
 | 
			
		||||
 | 
			
		||||
        # query overrides preferences
 | 
			
		||||
        preferences = {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_SHARED,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        query_dict = QueryDict('sort=title_desc&shared=no&unread=off')
 | 
			
		||||
 | 
			
		||||
        search = BookmarkSearch.from_request(query_dict, preferences)
 | 
			
		||||
        self.assertEqual(search.q, '')
 | 
			
		||||
        self.assertEqual(search.user, '')
 | 
			
		||||
        self.assertEqual(search.sort, BookmarkSearch.SORT_TITLE_DESC)
 | 
			
		||||
        self.assertEqual(search.shared, BookmarkSearch.FILTER_SHARED_UNSHARED)
 | 
			
		||||
        self.assertEqual(search.unread, BookmarkSearch.FILTER_UNREAD_OFF)
 | 
			
		||||
 | 
			
		||||
    def test_modified_params(self):
 | 
			
		||||
        # no params
 | 
			
		||||
        bookmark_search = BookmarkSearch()
 | 
			
		||||
@@ -69,3 +89,74 @@ class BookmarkSearchModelTest(TestCase):
 | 
			
		||||
                                         unread=BookmarkSearch.FILTER_UNREAD_YES)
 | 
			
		||||
        modified_params = bookmark_search.modified_params
 | 
			
		||||
        self.assertCountEqual(modified_params, ['q', 'sort', 'user', 'shared', 'unread'])
 | 
			
		||||
 | 
			
		||||
        # preferences are not modified params
 | 
			
		||||
        preferences = {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        bookmark_search = BookmarkSearch(preferences=preferences)
 | 
			
		||||
        modified_params = bookmark_search.modified_params
 | 
			
		||||
        self.assertEqual(len(modified_params), 0)
 | 
			
		||||
 | 
			
		||||
        # param is not modified if it matches the preference
 | 
			
		||||
        preferences = {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
                                         unread=BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
                                         preferences=preferences)
 | 
			
		||||
        modified_params = bookmark_search.modified_params
 | 
			
		||||
        self.assertEqual(len(modified_params), 0)
 | 
			
		||||
 | 
			
		||||
        # overriding preferences is a modified param
 | 
			
		||||
        preferences = {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_SHARED,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC,
 | 
			
		||||
                                         shared=BookmarkSearch.FILTER_SHARED_UNSHARED,
 | 
			
		||||
                                         unread=BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
                                         preferences=preferences)
 | 
			
		||||
        modified_params = bookmark_search.modified_params
 | 
			
		||||
        self.assertCountEqual(modified_params, ['sort', 'shared', 'unread'])
 | 
			
		||||
 | 
			
		||||
    def test_has_modifications(self):
 | 
			
		||||
        # no params
 | 
			
		||||
        bookmark_search = BookmarkSearch()
 | 
			
		||||
        self.assertFalse(bookmark_search.has_modifications)
 | 
			
		||||
 | 
			
		||||
        # params are default values
 | 
			
		||||
        bookmark_search = BookmarkSearch(q='', sort=BookmarkSearch.SORT_ADDED_DESC, user='', shared='')
 | 
			
		||||
        self.assertFalse(bookmark_search.has_modifications)
 | 
			
		||||
 | 
			
		||||
        # modified params
 | 
			
		||||
        bookmark_search = BookmarkSearch(q='search query', sort=BookmarkSearch.SORT_ADDED_ASC)
 | 
			
		||||
        self.assertTrue(bookmark_search.has_modifications)
 | 
			
		||||
 | 
			
		||||
    def test_preferences_dict(self):
 | 
			
		||||
        # no params
 | 
			
		||||
        bookmark_search = BookmarkSearch()
 | 
			
		||||
        self.assertEqual(bookmark_search.preferences_dict, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # with params
 | 
			
		||||
        bookmark_search = BookmarkSearch(sort=BookmarkSearch.SORT_TITLE_DESC, unread=BookmarkSearch.FILTER_UNREAD_YES)
 | 
			
		||||
        self.assertEqual(bookmark_search.preferences_dict, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # only returns preferences
 | 
			
		||||
        bookmark_search = BookmarkSearch(q='search query', user='user123')
 | 
			
		||||
        self.assertEqual(bookmark_search.preferences_dict, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 
 | 
			
		||||
@@ -1,58 +1,226 @@
 | 
			
		||||
from bs4 import BeautifulSoup
 | 
			
		||||
from django.db.models import QuerySet
 | 
			
		||||
from django.template import Template, RequestContext
 | 
			
		||||
from django.test import TestCase, RequestFactory
 | 
			
		||||
 | 
			
		||||
from bookmarks.models import BookmarkSearch, Tag
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all()):
 | 
			
		||||
class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
    def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all(), mode: str = ''):
 | 
			
		||||
        rf = RequestFactory()
 | 
			
		||||
        request = rf.get(url)
 | 
			
		||||
        request.user = self.get_or_create_test_user()
 | 
			
		||||
        request.user_profile = self.get_or_create_test_user().profile
 | 
			
		||||
        search = BookmarkSearch.from_request(request)
 | 
			
		||||
        search = BookmarkSearch.from_request(request.GET)
 | 
			
		||||
        context = RequestContext(request, {
 | 
			
		||||
            'request': request,
 | 
			
		||||
            'search': search,
 | 
			
		||||
            'tags': tags,
 | 
			
		||||
            'mode': mode,
 | 
			
		||||
        })
 | 
			
		||||
        template_to_render = Template(
 | 
			
		||||
            '{% load bookmarks %}'
 | 
			
		||||
            '{% bookmark_search search tags %}'
 | 
			
		||||
            '{% bookmark_search search tags mode %}'
 | 
			
		||||
        )
 | 
			
		||||
        return template_to_render.render(context)
 | 
			
		||||
 | 
			
		||||
    def assertHiddenInput(self, html: str, name: str, value: str = None):
 | 
			
		||||
        needle = f'<input type="hidden" name="{name}"'
 | 
			
		||||
    def assertHiddenInput(self, form: BeautifulSoup, name: str, value: str = None):
 | 
			
		||||
        input = form.select_one(f'input[name="{name}"][type="hidden"]')
 | 
			
		||||
        self.assertIsNotNone(input)
 | 
			
		||||
 | 
			
		||||
        if value is not None:
 | 
			
		||||
            needle += f' value="{value}"'
 | 
			
		||||
            self.assertEqual(input['value'], value)
 | 
			
		||||
 | 
			
		||||
        self.assertIn(needle, html)
 | 
			
		||||
    def assertNoHiddenInput(self, form: BeautifulSoup, name: str):
 | 
			
		||||
        input = form.select_one(f'input[name="{name}"][type="hidden"]')
 | 
			
		||||
        self.assertIsNone(input)
 | 
			
		||||
 | 
			
		||||
    def assertNoHiddenInput(self, html: str, name: str):
 | 
			
		||||
        needle = f'<input type="hidden" name="{name}"'
 | 
			
		||||
    def assertSearchInput(self, form: BeautifulSoup, name: str, value: str = None):
 | 
			
		||||
        input = form.select_one(f'input[name="{name}"][type="search"]')
 | 
			
		||||
        self.assertIsNotNone(input)
 | 
			
		||||
 | 
			
		||||
        self.assertNotIn(needle, html)
 | 
			
		||||
        if value is not None:
 | 
			
		||||
            self.assertEqual(input['value'], value)
 | 
			
		||||
 | 
			
		||||
    def test_hidden_inputs(self):
 | 
			
		||||
    def assertSelect(self, form: BeautifulSoup, name: str, value: str = None):
 | 
			
		||||
        select = form.select_one(f'select[name="{name}"]')
 | 
			
		||||
        self.assertIsNotNone(select)
 | 
			
		||||
 | 
			
		||||
        if value is not None:
 | 
			
		||||
            options = select.select('option')
 | 
			
		||||
            for option in options:
 | 
			
		||||
                if option['value'] == value:
 | 
			
		||||
                    self.assertTrue(option.has_attr('selected'))
 | 
			
		||||
                else:
 | 
			
		||||
                    self.assertFalse(option.has_attr('selected'))
 | 
			
		||||
 | 
			
		||||
    def assertRadioGroup(self, form: BeautifulSoup, name: str, value: str = None):
 | 
			
		||||
        radios = form.select(f'input[name="{name}"][type="radio"]')
 | 
			
		||||
        self.assertTrue(len(radios) > 0)
 | 
			
		||||
 | 
			
		||||
        if value is not None:
 | 
			
		||||
            for radio in radios:
 | 
			
		||||
                if radio['value'] == value:
 | 
			
		||||
                    self.assertTrue(radio.has_attr('checked'))
 | 
			
		||||
                else:
 | 
			
		||||
                    self.assertFalse(radio.has_attr('checked'))
 | 
			
		||||
 | 
			
		||||
    def assertNoRadioGroup(self, form: BeautifulSoup, name: str):
 | 
			
		||||
        radios = form.select(f'input[name="{name}"][type="radio"]')
 | 
			
		||||
        self.assertTrue(len(radios) == 0)
 | 
			
		||||
 | 
			
		||||
    def assertUnmodifiedLabel(self, html: str, text: str, id: str = ''):
 | 
			
		||||
        id_attr = f'for="{id}"' if id else ''
 | 
			
		||||
        tag = 'label' if id else 'div'
 | 
			
		||||
        needle = f'<{tag} class="form-label" {id_attr}>{text}</{tag}>'
 | 
			
		||||
 | 
			
		||||
        self.assertInHTML(needle, html)
 | 
			
		||||
 | 
			
		||||
    def assertModifiedLabel(self, html: str, text: str, id: str = ''):
 | 
			
		||||
        id_attr = f'for="{id}"' if id else ''
 | 
			
		||||
        tag = 'label' if id else 'div'
 | 
			
		||||
        needle = f'<{tag} class="form-label text-bold" {id_attr}>{text}</{tag}>'
 | 
			
		||||
 | 
			
		||||
        self.assertInHTML(needle, html)
 | 
			
		||||
 | 
			
		||||
    def test_search_form_inputs(self):
 | 
			
		||||
        # Without params
 | 
			
		||||
        url = '/test'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
        soup = self.make_soup(rendered_template)
 | 
			
		||||
        search_form = soup.select_one('form#search')
 | 
			
		||||
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'user')
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'q')
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'sort')
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'shared')
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'unread')
 | 
			
		||||
        self.assertSearchInput(search_form, 'q')
 | 
			
		||||
        self.assertNoHiddenInput(search_form, 'user')
 | 
			
		||||
        self.assertNoHiddenInput(search_form, 'sort')
 | 
			
		||||
        self.assertNoHiddenInput(search_form, 'shared')
 | 
			
		||||
        self.assertNoHiddenInput(search_form, 'unread')
 | 
			
		||||
 | 
			
		||||
        # With params
 | 
			
		||||
        url = '/test?q=foo&user=john&sort=title_asc&shared=shared&unread=yes'
 | 
			
		||||
        url = '/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
        soup = self.make_soup(rendered_template)
 | 
			
		||||
        search_form = soup.select_one('form#search')
 | 
			
		||||
 | 
			
		||||
        self.assertSearchInput(search_form, 'q', 'foo')
 | 
			
		||||
        self.assertHiddenInput(search_form, 'user', 'john')
 | 
			
		||||
        self.assertHiddenInput(search_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
 | 
			
		||||
        self.assertHiddenInput(search_form, 'shared', BookmarkSearch.FILTER_SHARED_SHARED)
 | 
			
		||||
        self.assertHiddenInput(search_form, 'unread', BookmarkSearch.FILTER_UNREAD_YES)
 | 
			
		||||
 | 
			
		||||
    def test_preferences_form_inputs(self):
 | 
			
		||||
        # Without params
 | 
			
		||||
        url = '/test'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
        soup = self.make_soup(rendered_template)
 | 
			
		||||
        preferences_form = soup.select_one('form#search_preferences')
 | 
			
		||||
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'q')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'user')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'sort')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'shared')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'unread')
 | 
			
		||||
 | 
			
		||||
        self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_ADDED_DESC)
 | 
			
		||||
        self.assertRadioGroup(preferences_form, 'shared', BookmarkSearch.FILTER_SHARED_OFF)
 | 
			
		||||
        self.assertRadioGroup(preferences_form, 'unread', BookmarkSearch.FILTER_UNREAD_OFF)
 | 
			
		||||
 | 
			
		||||
        # With params
 | 
			
		||||
        url = '/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
        soup = self.make_soup(rendered_template)
 | 
			
		||||
        preferences_form = soup.select_one('form#search_preferences')
 | 
			
		||||
 | 
			
		||||
        self.assertHiddenInput(preferences_form, 'q', 'foo')
 | 
			
		||||
        self.assertHiddenInput(preferences_form, 'user', 'john')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'sort')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'shared')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'unread')
 | 
			
		||||
 | 
			
		||||
        self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
 | 
			
		||||
        self.assertRadioGroup(preferences_form, 'shared', BookmarkSearch.FILTER_SHARED_SHARED)
 | 
			
		||||
        self.assertRadioGroup(preferences_form, 'unread', BookmarkSearch.FILTER_UNREAD_YES)
 | 
			
		||||
 | 
			
		||||
    def test_preferences_form_inputs_shared_mode(self):
 | 
			
		||||
        # Without params
 | 
			
		||||
        url = '/test'
 | 
			
		||||
        rendered_template = self.render_template(url, mode='shared')
 | 
			
		||||
        soup = self.make_soup(rendered_template)
 | 
			
		||||
        preferences_form = soup.select_one('form#search_preferences')
 | 
			
		||||
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'q')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'user')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'sort')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'shared')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'unread')
 | 
			
		||||
 | 
			
		||||
        self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_ADDED_DESC)
 | 
			
		||||
        self.assertNoRadioGroup(preferences_form, 'shared')
 | 
			
		||||
        self.assertNoRadioGroup(preferences_form, 'unread')
 | 
			
		||||
 | 
			
		||||
        # With params
 | 
			
		||||
        url = '/test?q=foo&user=john&sort=title_asc'
 | 
			
		||||
        rendered_template = self.render_template(url, mode='shared')
 | 
			
		||||
        soup = self.make_soup(rendered_template)
 | 
			
		||||
        preferences_form = soup.select_one('form#search_preferences')
 | 
			
		||||
 | 
			
		||||
        self.assertHiddenInput(preferences_form, 'q', 'foo')
 | 
			
		||||
        self.assertHiddenInput(preferences_form, 'user', 'john')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'sort')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'shared')
 | 
			
		||||
        self.assertNoHiddenInput(preferences_form, 'unread')
 | 
			
		||||
 | 
			
		||||
        self.assertSelect(preferences_form, 'sort', BookmarkSearch.SORT_TITLE_ASC)
 | 
			
		||||
        self.assertNoRadioGroup(preferences_form, 'shared')
 | 
			
		||||
        self.assertNoRadioGroup(preferences_form, 'unread')
 | 
			
		||||
 | 
			
		||||
    def test_modified_indicator(self):
 | 
			
		||||
        # Without modifications
 | 
			
		||||
        url = '/test'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
 | 
			
		||||
        self.assertHiddenInput(rendered_template, 'user', 'john')
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'q')
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'sort')
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'shared')
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'unread')
 | 
			
		||||
        self.assertIn('<button type="button" class="btn dropdown-toggle">', rendered_template)
 | 
			
		||||
 | 
			
		||||
        # With modifications
 | 
			
		||||
        url = '/test?sort=title_asc'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
 | 
			
		||||
        self.assertIn('<button type="button" class="btn dropdown-toggle badge">', rendered_template)
 | 
			
		||||
 | 
			
		||||
        # Ignores non-preferences modifications
 | 
			
		||||
        url = '/test?q=foo&user=john'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
 | 
			
		||||
        self.assertIn('<button type="button" class="btn dropdown-toggle">', rendered_template)
 | 
			
		||||
 | 
			
		||||
    def test_modified_labels(self):
 | 
			
		||||
        # Without modifications
 | 
			
		||||
        url = '/test'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
 | 
			
		||||
 | 
			
		||||
        # Modified sort
 | 
			
		||||
        url = '/test?sort=title_asc'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
        self.assertModifiedLabel(rendered_template, 'Sort by', 'id_sort')
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
 | 
			
		||||
 | 
			
		||||
        # Modified shared
 | 
			
		||||
        url = '/test?shared=yes'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
 | 
			
		||||
        self.assertModifiedLabel(rendered_template, 'Shared filter')
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Unread filter')
 | 
			
		||||
 | 
			
		||||
        # Modified unread
 | 
			
		||||
        url = '/test?unread=yes'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Sort by', 'id_sort')
 | 
			
		||||
        self.assertUnmodifiedLabel(rendered_template, 'Shared filter')
 | 
			
		||||
        self.assertModifiedLabel(rendered_template, 'Unread filter')
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@ from django.contrib.auth.models import User
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from bookmarks.models import Bookmark, Tag, UserProfile
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, collapse_whitespace, HtmlTestMixin
 | 
			
		||||
from bookmarks.models import Bookmark, BookmarkSearch, Tag, UserProfile
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
@@ -22,27 +22,47 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
        self.assertContains(response, '<li ld-bookmark-item class="shared">', count=len(bookmarks))
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        bookmark_list = soup.select_one(f'ul.bookmark-list[data-bookmarks-total="{len(bookmarks)}"]')
 | 
			
		||||
        self.assertIsNotNone(bookmark_list)
 | 
			
		||||
 | 
			
		||||
        bookmark_items = bookmark_list.select('li[ld-bookmark-item]')
 | 
			
		||||
        self.assertEqual(len(bookmark_items), len(bookmarks))
 | 
			
		||||
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
            self.assertBookmarkCount(html, bookmark, 1, link_target)
 | 
			
		||||
            bookmark_item = bookmark_list.select_one(
 | 
			
		||||
                f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
 | 
			
		||||
            self.assertIsNotNone(bookmark_item)
 | 
			
		||||
 | 
			
		||||
    def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
            self.assertBookmarkCount(html, bookmark, 0, link_target)
 | 
			
		||||
            bookmark_item = soup.select_one(
 | 
			
		||||
                f'li[ld-bookmark-item] a[href="{bookmark.url}"][target="{link_target}"]')
 | 
			
		||||
            self.assertIsNone(bookmark_item)
 | 
			
		||||
 | 
			
		||||
    def assertVisibleTags(self, response, tags: [Tag]):
 | 
			
		||||
        self.assertContains(response, 'data-is-tag-item', count=len(tags))
 | 
			
		||||
    def assertVisibleTags(self, response, tags: List[Tag]):
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        tag_cloud = soup.select_one('div.tag-cloud')
 | 
			
		||||
        self.assertIsNotNone(tag_cloud)
 | 
			
		||||
 | 
			
		||||
        tag_items = tag_cloud.select('a[data-is-tag-item]')
 | 
			
		||||
        self.assertEqual(len(tag_items), len(tags))
 | 
			
		||||
 | 
			
		||||
        tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
 | 
			
		||||
 | 
			
		||||
        for tag in tags:
 | 
			
		||||
            self.assertContains(response, tag.name)
 | 
			
		||||
            self.assertTrue(tag.name in tag_item_names)
 | 
			
		||||
 | 
			
		||||
    def assertInvisibleTags(self, response, tags: List[Tag]):
 | 
			
		||||
        soup = self.make_soup(response.content.decode())
 | 
			
		||||
        tag_items = soup.select('a[data-is-tag-item]')
 | 
			
		||||
 | 
			
		||||
        tag_item_names = [tag_item.text.strip() for tag_item in tag_items]
 | 
			
		||||
 | 
			
		||||
    def assertInvisibleTags(self, response, tags: [Tag]):
 | 
			
		||||
        for tag in tags:
 | 
			
		||||
            self.assertNotContains(response, tag.name)
 | 
			
		||||
            self.assertFalse(tag.name in tag_item_names)
 | 
			
		||||
 | 
			
		||||
    def assertVisibleUserOptions(self, response, users: List[User]):
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
@@ -86,10 +106,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
        html = collapse_whitespace(response.content.decode())
 | 
			
		||||
 | 
			
		||||
        # Should render list
 | 
			
		||||
        self.assertIn('<ul class="bookmark-list" data-bookmarks-total="3">', html)
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
@@ -116,22 +133,12 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
    def test_should_list_bookmarks_matching_query(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user = self.setup_user(enable_sharing=True)
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(shared=True, title='searchvalue', user=user),
 | 
			
		||||
            self.setup_bookmark(shared=True, title='searchvalue', user=user),
 | 
			
		||||
            self.setup_bookmark(shared=True, title='searchvalue', user=user)
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user),
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user),
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared') + '?q=searchvalue')
 | 
			
		||||
        html = collapse_whitespace(response.content.decode())
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user, prefix='foo')
 | 
			
		||||
        invisible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared') + '?q=foo')
 | 
			
		||||
 | 
			
		||||
        # Should render list
 | 
			
		||||
        self.assertIn('<ul class="bookmark-list" data-bookmarks-total="3">', html)
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
@@ -139,22 +146,11 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user1),
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user1),
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user1),
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user2),
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user2),
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user2),
 | 
			
		||||
        ]
 | 
			
		||||
        visible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user1, prefix='user1')
 | 
			
		||||
        invisible_bookmarks = self.setup_numbered_bookmarks(3, shared=True, user=user2, prefix='user2')
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
        html = collapse_whitespace(response.content.decode())
 | 
			
		||||
 | 
			
		||||
        # Should render list
 | 
			
		||||
        self.assertIn('<ul class="bookmark-list" data-bookmarks-total="3">', html)
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
@@ -297,6 +293,30 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
        self.assertVisibleUserOptions(response, expected_visible_users)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_bookmarks_and_tags_for_search_preferences(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        other_user = self.setup_user(enable_sharing=True)
 | 
			
		||||
 | 
			
		||||
        user_profile = self.get_or_create_test_user().profile
 | 
			
		||||
        user_profile.search_preferences = {
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        }
 | 
			
		||||
        user_profile.save()
 | 
			
		||||
 | 
			
		||||
        unread_bookmarks = self.setup_numbered_bookmarks(3, shared=True, unread=True, with_tags=True, prefix='unread',
 | 
			
		||||
                                                         tag_prefix='unread', user=other_user)
 | 
			
		||||
        read_bookmarks = self.setup_numbered_bookmarks(3, shared=True, unread=False, with_tags=True, prefix='read',
 | 
			
		||||
                                                       tag_prefix='read', user=other_user)
 | 
			
		||||
 | 
			
		||||
        unread_tags = self.get_tags_from_bookmarks(unread_bookmarks)
 | 
			
		||||
        read_tags = self.get_tags_from_bookmarks(read_bookmarks)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
        self.assertVisibleBookmarks(response, unread_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, read_bookmarks)
 | 
			
		||||
        self.assertVisibleTags(response, unread_tags)
 | 
			
		||||
        self.assertInvisibleTags(response, read_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_open_bookmarks_in_new_page_by_default(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user = self.get_or_create_test_user()
 | 
			
		||||
@@ -370,6 +390,107 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        response = self.client.get(base_url + url_params)
 | 
			
		||||
        self.assertEditLink(response, url)
 | 
			
		||||
 | 
			
		||||
    def test_apply_search_preferences(self):
 | 
			
		||||
        # no params
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:shared'))
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:shared'))
 | 
			
		||||
 | 
			
		||||
        # some params
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:shared'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&sort=title_asc')
 | 
			
		||||
 | 
			
		||||
        # params with default value are removed
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:shared'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'user': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&unread=yes')
 | 
			
		||||
 | 
			
		||||
        # page is removed
 | 
			
		||||
        response = self.client.post(reverse('bookmarks:shared'), {
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'page': '2',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse('bookmarks:shared') + '?q=foo&sort=title_asc')
 | 
			
		||||
 | 
			
		||||
    def test_save_search_preferences(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user_profile = self.user.profile
 | 
			
		||||
 | 
			
		||||
        # no params
 | 
			
		||||
        self.client.post(reverse('bookmarks:shared'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # with param
 | 
			
		||||
        self.client.post(reverse('bookmarks:shared'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # add a param
 | 
			
		||||
        self.client.post(reverse('bookmarks:shared'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # remove a param
 | 
			
		||||
        self.client.post(reverse('bookmarks:shared'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_ADDED_DESC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_YES,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        # ignores non-preferences
 | 
			
		||||
        self.client.post(reverse('bookmarks:shared'), {
 | 
			
		||||
            'save': '',
 | 
			
		||||
            'q': 'foo',
 | 
			
		||||
            'user': 'john',
 | 
			
		||||
            'page': '3',
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
        })
 | 
			
		||||
        user_profile.refresh_from_db()
 | 
			
		||||
        self.assertEqual(user_profile.search_preferences, {
 | 
			
		||||
            'sort': BookmarkSearch.SORT_TITLE_ASC,
 | 
			
		||||
            'shared': BookmarkSearch.FILTER_SHARED_OFF,
 | 
			
		||||
            'unread': BookmarkSearch.FILTER_UNREAD_OFF,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_url_encode_bookmark_actions_url(self):
 | 
			
		||||
        url = reverse('bookmarks:shared') + '?q=%23foo'
 | 
			
		||||
        response = self.client.get(url)
 | 
			
		||||
 
 | 
			
		||||
@@ -99,12 +99,12 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], unshared_bookmarks + shared_bookmarks)
 | 
			
		||||
 | 
			
		||||
        # Filter shared
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-list') + '?shared=shared',
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-list') + '?shared=yes',
 | 
			
		||||
                            expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)
 | 
			
		||||
 | 
			
		||||
        # Filter unshared
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-list') + '?shared=unshared',
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-list') + '?shared=no',
 | 
			
		||||
                            expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], unshared_bookmarks)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -146,12 +146,6 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            self.setup_bookmark(tags=[tag1, tag2, self.setup_tag()]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def get_tags_from_bookmarks(self, bookmarks: [Bookmark]):
 | 
			
		||||
        all_tags = []
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
            all_tags = all_tags + list(bookmark.tags.all())
 | 
			
		||||
        return all_tags
 | 
			
		||||
 | 
			
		||||
    def assertQueryResult(self, query: QuerySet, item_lists: [[any]]):
 | 
			
		||||
        expected_items = []
 | 
			
		||||
        for item_list in item_lists:
 | 
			
		||||
@@ -164,7 +158,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmarks_should_return_all_for_empty_query(self):
 | 
			
		||||
        self.setup_bookmark_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.other_bookmarks,
 | 
			
		||||
            self.term1_bookmarks,
 | 
			
		||||
@@ -179,7 +173,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmarks_should_search_single_term(self):
 | 
			
		||||
        self.setup_bookmark_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='term1'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.term1_bookmarks,
 | 
			
		||||
            self.term1_term2_bookmarks,
 | 
			
		||||
@@ -189,35 +183,35 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmarks_should_search_multiple_terms(self):
 | 
			
		||||
        self.setup_bookmark_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term2 term1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='term2 term1'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [self.term1_term2_bookmarks])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_should_search_single_tag(self):
 | 
			
		||||
        self.setup_bookmark_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='#tag1'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [self.tag1_bookmarks, self.tag1_tag2_bookmarks, self.term1_tag1_bookmarks])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_should_search_multiple_tags(self):
 | 
			
		||||
        self.setup_bookmark_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1 #tag2'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='#tag1 #tag2'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [self.tag1_tag2_bookmarks])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_should_search_multiple_tags_ignoring_casing(self):
 | 
			
		||||
        self.setup_bookmark_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#Tag1 #TAG2'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='#Tag1 #TAG2'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [self.tag1_tag2_bookmarks])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_should_search_terms_and_tags_combined(self):
 | 
			
		||||
        self.setup_bookmark_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #tag1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='term1 #tag1'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [self.term1_tag1_bookmarks])
 | 
			
		||||
 | 
			
		||||
@@ -227,7 +221,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.profile.tag_search = UserProfile.TAG_SEARCH_STRICT
 | 
			
		||||
        self.profile.save()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [self.tag1_as_term_bookmarks])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_in_lax_mode_should_search_tags_as_terms(self):
 | 
			
		||||
@@ -236,7 +230,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.profile.tag_search = UserProfile.TAG_SEARCH_LAX
 | 
			
		||||
        self.profile.save()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.tag1_bookmarks,
 | 
			
		||||
            self.tag1_as_term_bookmarks,
 | 
			
		||||
@@ -244,17 +238,17 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            self.term1_tag1_bookmarks
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 term1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='tag1 term1'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.term1_tag1_bookmarks,
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 tag2'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='tag1 tag2'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.tag1_tag2_bookmarks,
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='tag1 #tag2'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='tag1 #tag2'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.tag1_tag2_bookmarks,
 | 
			
		||||
        ])
 | 
			
		||||
@@ -262,28 +256,28 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmarks_should_return_no_matches(self):
 | 
			
		||||
        self.setup_bookmark_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term3'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='term3'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 term3'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='term1 term3'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #tag2'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='term1 #tag2'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag3'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='#tag3'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        # Unused tag
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#unused_tag1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='#unused_tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        # Unused tag combined with tag that is used
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='#tag1 #unused_tag1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='#tag1 #unused_tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        # Unused tag combined with term that is used
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='term1 #unused_tag1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='term1 #unused_tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_should_not_return_archived_bookmarks(self):
 | 
			
		||||
@@ -293,7 +287,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(is_archived=True)
 | 
			
		||||
        self.setup_bookmark(is_archived=True)
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [[bookmark1, bookmark2]])
 | 
			
		||||
 | 
			
		||||
@@ -304,7 +298,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark()
 | 
			
		||||
        self.setup_bookmark()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [[bookmark1, bookmark2]])
 | 
			
		||||
 | 
			
		||||
@@ -319,7 +313,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(user=other_user)
 | 
			
		||||
        self.setup_bookmark(user=other_user)
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [owned_bookmarks])
 | 
			
		||||
 | 
			
		||||
@@ -334,7 +328,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(is_archived=True, user=other_user)
 | 
			
		||||
        self.setup_bookmark(is_archived=True, user=other_user)
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [owned_bookmarks])
 | 
			
		||||
 | 
			
		||||
@@ -344,7 +338,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(tags=[tag])
 | 
			
		||||
        self.setup_bookmark(tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='!untagged'))
 | 
			
		||||
        self.assertCountEqual(list(query), [untagged_bookmark])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_untagged_should_be_combinable_with_search_terms(self):
 | 
			
		||||
@@ -353,7 +347,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(title='term2')
 | 
			
		||||
        self.setup_bookmark(tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged term1'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='!untagged term1'))
 | 
			
		||||
        self.assertCountEqual(list(query), [untagged_bookmark])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_untagged_should_not_be_combinable_with_tags(self):
 | 
			
		||||
@@ -362,7 +356,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(tags=[tag])
 | 
			
		||||
        self.setup_bookmark(tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query=f'!untagged #{tag.name}'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q=f'!untagged #{tag.name}'))
 | 
			
		||||
        self.assertCountEqual(list(query), [])
 | 
			
		||||
 | 
			
		||||
    def test_query_archived_bookmarks_untagged_should_return_untagged_bookmarks_only(self):
 | 
			
		||||
@@ -371,7 +365,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged'))
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(q='!untagged'))
 | 
			
		||||
        self.assertCountEqual(list(query), [untagged_bookmark])
 | 
			
		||||
 | 
			
		||||
    def test_query_archived_bookmarks_untagged_should_be_combinable_with_search_terms(self):
 | 
			
		||||
@@ -380,7 +374,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(is_archived=True, title='term2')
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!untagged term1'))
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(q='!untagged term1'))
 | 
			
		||||
        self.assertCountEqual(list(query), [untagged_bookmark])
 | 
			
		||||
 | 
			
		||||
    def test_query_archived_bookmarks_untagged_should_not_be_combinable_with_tags(self):
 | 
			
		||||
@@ -390,7 +384,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile,
 | 
			
		||||
                                                 BookmarkSearch(query=f'!untagged #{tag.name}'))
 | 
			
		||||
                                                 BookmarkSearch(q=f'!untagged #{tag.name}'))
 | 
			
		||||
        self.assertCountEqual(list(query), [])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmarks_unread_should_return_unread_bookmarks_only(self):
 | 
			
		||||
@@ -398,7 +392,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        read_bookmarks = self.setup_numbered_bookmarks(5, unread=False)
 | 
			
		||||
 | 
			
		||||
        # Legacy query filter
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread'))
 | 
			
		||||
        query = queries.query_bookmarks(self.user, self.profile, BookmarkSearch(q='!unread'))
 | 
			
		||||
        self.assertCountEqual(list(query), unread_bookmarks)
 | 
			
		||||
 | 
			
		||||
        # Bookmark search filter - off
 | 
			
		||||
@@ -421,7 +415,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        read_bookmarks = self.setup_numbered_bookmarks(5, unread=False, archived=True)
 | 
			
		||||
 | 
			
		||||
        # Legacy query filter
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(query='!unread'))
 | 
			
		||||
        query = queries.query_archived_bookmarks(self.user, self.profile, BookmarkSearch(q='!unread'))
 | 
			
		||||
        self.assertCountEqual(list(query), unread_bookmarks)
 | 
			
		||||
 | 
			
		||||
        # Bookmark search filter - off
 | 
			
		||||
@@ -461,7 +455,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmark_tags_should_return_all_tags_for_empty_query(self):
 | 
			
		||||
        self.setup_tag_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.other_bookmarks),
 | 
			
		||||
@@ -476,7 +470,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmark_tags_should_search_single_term(self):
 | 
			
		||||
        self.setup_tag_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='term1'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.term1_bookmarks),
 | 
			
		||||
@@ -487,7 +481,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmark_tags_should_search_multiple_terms(self):
 | 
			
		||||
        self.setup_tag_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term2 term1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='term2 term1'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
 | 
			
		||||
@@ -496,7 +490,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmark_tags_should_search_single_tag(self):
 | 
			
		||||
        self.setup_tag_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='#tag1'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.tag1_bookmarks),
 | 
			
		||||
@@ -507,7 +501,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmark_tags_should_search_multiple_tags(self):
 | 
			
		||||
        self.setup_tag_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1 #tag2'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='#tag1 #tag2'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
 | 
			
		||||
@@ -516,7 +510,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmark_tags_should_search_multiple_tags_ignoring_casing(self):
 | 
			
		||||
        self.setup_tag_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#Tag1 #TAG2'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='#Tag1 #TAG2'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
 | 
			
		||||
@@ -525,7 +519,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmark_tags_should_search_term_and_tag_combined(self):
 | 
			
		||||
        self.setup_tag_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #tag1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='term1 #tag1'))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
 | 
			
		||||
@@ -537,7 +531,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.profile.tag_search = UserProfile.TAG_SEARCH_STRICT
 | 
			
		||||
        self.profile.save()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='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):
 | 
			
		||||
@@ -546,7 +540,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.profile.tag_search = UserProfile.TAG_SEARCH_LAX
 | 
			
		||||
        self.profile.save()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.tag1_bookmarks),
 | 
			
		||||
            self.get_tags_from_bookmarks(self.tag1_as_term_bookmarks),
 | 
			
		||||
@@ -554,17 +548,17 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks)
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 term1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='tag1 term1'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 tag2'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='tag1 tag2'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='tag1 #tag2'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='tag1 #tag2'))
 | 
			
		||||
        self.assertQueryResult(query, [
 | 
			
		||||
            self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
 | 
			
		||||
        ])
 | 
			
		||||
@@ -572,28 +566,28 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_query_bookmark_tags_should_return_no_matches(self):
 | 
			
		||||
        self.setup_tag_search_data()
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term3'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='term3'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 term3'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='term1 term3'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #tag2'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='term1 #tag2'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag3'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='#tag3'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        # Unused tag
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#unused_tag1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='#unused_tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        # Unused tag combined with tag that is used
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='#tag1 #unused_tag1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='#tag1 #unused_tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
        # Unused tag combined with term that is used
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='term1 #unused_tag1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='term1 #unused_tag1'))
 | 
			
		||||
        self.assertQueryResult(query, [])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmark_tags_should_return_tags_for_unarchived_bookmarks_only(self):
 | 
			
		||||
@@ -603,7 +597,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark()
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag2])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [[tag1]])
 | 
			
		||||
 | 
			
		||||
@@ -613,7 +607,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(tags=[tag])
 | 
			
		||||
        self.setup_bookmark(tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [[tag]])
 | 
			
		||||
 | 
			
		||||
@@ -624,7 +618,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark()
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag2])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [[tag2]])
 | 
			
		||||
 | 
			
		||||
@@ -634,7 +628,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [[tag]])
 | 
			
		||||
 | 
			
		||||
@@ -649,7 +643,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)])
 | 
			
		||||
        self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
 | 
			
		||||
 | 
			
		||||
@@ -664,7 +658,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query=''))
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(q=''))
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
 | 
			
		||||
 | 
			
		||||
@@ -675,13 +669,13 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(title='term1', tags=[tag])
 | 
			
		||||
        self.setup_bookmark(tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='!untagged'))
 | 
			
		||||
        self.assertCountEqual(list(query), [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged term1'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='!untagged term1'))
 | 
			
		||||
        self.assertCountEqual(list(query), [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query=f'!untagged #{tag.name}'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q=f'!untagged #{tag.name}'))
 | 
			
		||||
        self.assertCountEqual(list(query), [])
 | 
			
		||||
 | 
			
		||||
    def test_query_archived_bookmark_tags_untagged_should_never_return_any_tags(self):
 | 
			
		||||
@@ -691,14 +685,14 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(is_archived=True, title='term1', tags=[tag])
 | 
			
		||||
        self.setup_bookmark(is_archived=True, tags=[tag])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged'))
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(q='!untagged'))
 | 
			
		||||
        self.assertCountEqual(list(query), [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!untagged term1'))
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile, BookmarkSearch(q='!untagged term1'))
 | 
			
		||||
        self.assertCountEqual(list(query), [])
 | 
			
		||||
 | 
			
		||||
        query = queries.query_archived_bookmark_tags(self.user, self.profile,
 | 
			
		||||
                                                     BookmarkSearch(query=f'!untagged #{tag.name}'))
 | 
			
		||||
                                                     BookmarkSearch(q=f'!untagged #{tag.name}'))
 | 
			
		||||
        self.assertCountEqual(list(query), [])
 | 
			
		||||
 | 
			
		||||
    def test_query_bookmark_tags_filter_unread(self):
 | 
			
		||||
@@ -708,7 +702,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        read_tags = self.get_tags_from_bookmarks(read_bookmarks)
 | 
			
		||||
 | 
			
		||||
        # Legacy query filter
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(query='!unread'))
 | 
			
		||||
        query = queries.query_bookmark_tags(self.user, self.profile, BookmarkSearch(q='!unread'))
 | 
			
		||||
        self.assertCountEqual(list(query), unread_tags)
 | 
			
		||||
 | 
			
		||||
        # Bookmark search filter - off
 | 
			
		||||
@@ -769,14 +763,14 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(user=user4, shared=True, tags=[tag]),
 | 
			
		||||
 | 
			
		||||
        # Should return shared bookmarks from all users
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=''), False)
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(q=''), False)
 | 
			
		||||
        self.assertQueryResult(query_set, [shared_bookmarks])
 | 
			
		||||
 | 
			
		||||
        # Should respect search query
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query='test title'), False)
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(q='test title'), False)
 | 
			
		||||
        self.assertQueryResult(query_set, [[shared_bookmarks[0]]])
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=f'#{tag.name}'), False)
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(q=f'#{tag.name}'), False)
 | 
			
		||||
        self.assertQueryResult(query_set, [[shared_bookmarks[2]]])
 | 
			
		||||
 | 
			
		||||
    def test_query_publicly_shared_bookmarks(self):
 | 
			
		||||
@@ -786,7 +780,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        bookmark1 = self.setup_bookmark(user=user1, shared=True)
 | 
			
		||||
        self.setup_bookmark(user=user2, shared=True)
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(query=''), True)
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, BookmarkSearch(q=''), True)
 | 
			
		||||
        self.assertQueryResult(query_set, [[bookmark1]])
 | 
			
		||||
 | 
			
		||||
    def test_query_shared_bookmark_tags(self):
 | 
			
		||||
@@ -810,7 +804,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(user=user3, shared=False, tags=[self.setup_tag(user=user3)]),
 | 
			
		||||
        self.setup_bookmark(user=user4, shared=True, tags=[self.setup_tag(user=user4)]),
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmark_tags(None, self.profile, BookmarkSearch(query=''), False)
 | 
			
		||||
        query_set = queries.query_shared_bookmark_tags(None, self.profile, BookmarkSearch(q=''), False)
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query_set, [shared_tags])
 | 
			
		||||
 | 
			
		||||
@@ -824,7 +818,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(user=user1, shared=True, tags=[tag1]),
 | 
			
		||||
        self.setup_bookmark(user=user2, shared=True, tags=[tag2]),
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmark_tags(None, self.profile, BookmarkSearch(query=''), True)
 | 
			
		||||
        query_set = queries.query_shared_bookmark_tags(None, self.profile, BookmarkSearch(q=''), True)
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query_set, [[tag1]])
 | 
			
		||||
 | 
			
		||||
@@ -849,11 +843,11 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(user=users_without_shared_bookmarks[2], shared=True),
 | 
			
		||||
 | 
			
		||||
        # Should return users with shared bookmarks
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query=''), False)
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(q=''), False)
 | 
			
		||||
        self.assertQueryResult(query_set, [users_with_shared_bookmarks])
 | 
			
		||||
 | 
			
		||||
        # Should respect search query
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query='test title'), False)
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(q='test title'), False)
 | 
			
		||||
        self.assertQueryResult(query_set, [[users_with_shared_bookmarks[0]]])
 | 
			
		||||
 | 
			
		||||
    def test_query_publicly_shared_bookmark_users(self):
 | 
			
		||||
@@ -863,7 +857,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.setup_bookmark(user=user1, shared=True)
 | 
			
		||||
        self.setup_bookmark(user=user2, shared=True)
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(query=''), True)
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, BookmarkSearch(q=''), True)
 | 
			
		||||
        self.assertQueryResult(query_set, [[user1]])
 | 
			
		||||
 | 
			
		||||
    def test_sorty_by_date_added_asc(self):
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        request = rf.get(url)
 | 
			
		||||
        request.user = self.get_or_create_test_user()
 | 
			
		||||
        request.user_profile = self.get_or_create_test_user().profile
 | 
			
		||||
        search = BookmarkSearch.from_request(request)
 | 
			
		||||
        search = BookmarkSearch.from_request(request.GET)
 | 
			
		||||
        context = RequestContext(request, {
 | 
			
		||||
            'request': request,
 | 
			
		||||
            'search': search,
 | 
			
		||||
@@ -82,11 +82,11 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'unread')
 | 
			
		||||
 | 
			
		||||
        # With params
 | 
			
		||||
        url = '/test?q=foo&user=john&sort=title_asc&shared=shared&unread=yes'
 | 
			
		||||
        url = '/test?q=foo&user=john&sort=title_asc&shared=yes&unread=yes'
 | 
			
		||||
        rendered_template = self.render_template(url)
 | 
			
		||||
 | 
			
		||||
        self.assertNoHiddenInput(rendered_template, 'user')
 | 
			
		||||
        self.assertHiddenInput(rendered_template, 'q', 'foo')
 | 
			
		||||
        self.assertHiddenInput(rendered_template, 'sort', 'title_asc')
 | 
			
		||||
        self.assertHiddenInput(rendered_template, 'shared', 'shared')
 | 
			
		||||
        self.assertHiddenInput(rendered_template, 'shared', 'yes')
 | 
			
		||||
        self.assertHiddenInput(rendered_template, 'unread', 'yes')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
import urllib.parse
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.decorators import login_required
 | 
			
		||||
from django.db.models import QuerySet
 | 
			
		||||
from django.http import HttpResponseRedirect, Http404, HttpResponseBadRequest
 | 
			
		||||
from django.http import HttpResponseRedirect, Http404, HttpResponseBadRequest, HttpResponseForbidden
 | 
			
		||||
from django.shortcuts import render
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
@@ -17,6 +19,9 @@ _default_page_size = 30
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def index(request):
 | 
			
		||||
    if request.method == 'POST':
 | 
			
		||||
        return search_action(request)
 | 
			
		||||
 | 
			
		||||
    bookmark_list = contexts.ActiveBookmarkListContext(request)
 | 
			
		||||
    tag_cloud = contexts.ActiveTagCloudContext(request)
 | 
			
		||||
    return render(request, 'bookmarks/index.html', {
 | 
			
		||||
@@ -27,6 +32,9 @@ def index(request):
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def archived(request):
 | 
			
		||||
    if request.method == 'POST':
 | 
			
		||||
        return search_action(request)
 | 
			
		||||
 | 
			
		||||
    bookmark_list = contexts.ArchivedBookmarkListContext(request)
 | 
			
		||||
    tag_cloud = contexts.ArchivedTagCloudContext(request)
 | 
			
		||||
    return render(request, 'bookmarks/archive.html', {
 | 
			
		||||
@@ -36,11 +44,13 @@ def archived(request):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def shared(request):
 | 
			
		||||
    search = BookmarkSearch.from_request(request)
 | 
			
		||||
    if request.method == 'POST':
 | 
			
		||||
        return search_action(request)
 | 
			
		||||
 | 
			
		||||
    bookmark_list = contexts.SharedBookmarkListContext(request)
 | 
			
		||||
    tag_cloud = contexts.SharedTagCloudContext(request)
 | 
			
		||||
    public_only = not request.user.is_authenticated
 | 
			
		||||
    users = queries.query_shared_bookmark_users(request.user_profile, search, public_only)
 | 
			
		||||
    users = queries.query_shared_bookmark_users(request.user_profile, bookmark_list.search, public_only)
 | 
			
		||||
    return render(request, 'bookmarks/shared.html', {
 | 
			
		||||
        'bookmark_list': bookmark_list,
 | 
			
		||||
        'tag_cloud': tag_cloud,
 | 
			
		||||
@@ -48,6 +58,23 @@ def shared(request):
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def search_action(request):
 | 
			
		||||
    if 'save' in request.POST:
 | 
			
		||||
        if not request.user.is_authenticated:
 | 
			
		||||
            return HttpResponseForbidden()
 | 
			
		||||
        search = BookmarkSearch.from_request(request.POST)
 | 
			
		||||
        request.user_profile.search_preferences = search.preferences_dict
 | 
			
		||||
        request.user_profile.save()
 | 
			
		||||
 | 
			
		||||
    # redirect to base url including new query params
 | 
			
		||||
    search = BookmarkSearch.from_request(request.POST, request.user_profile.search_preferences)
 | 
			
		||||
    base_url = request.path
 | 
			
		||||
    query_params = search.query_params
 | 
			
		||||
    query_string = urllib.parse.urlencode(query_params)
 | 
			
		||||
    url = base_url if not query_string else base_url + '?' + query_string
 | 
			
		||||
    return HttpResponseRedirect(url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_tag_string(tag_string: str):
 | 
			
		||||
    # Tag strings coming from inputs are space-separated, however services.bookmarks functions expect comma-separated
 | 
			
		||||
    # strings
 | 
			
		||||
@@ -169,14 +196,14 @@ def mark_as_read(request, bookmark_id: int):
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def index_action(request):
 | 
			
		||||
    search = BookmarkSearch.from_request(request)
 | 
			
		||||
    search = BookmarkSearch.from_request(request.GET)
 | 
			
		||||
    query = queries.query_bookmarks(request.user, request.user_profile, search)
 | 
			
		||||
    return action(request, query)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def archived_action(request):
 | 
			
		||||
    search = BookmarkSearch.from_request(request)
 | 
			
		||||
    search = BookmarkSearch.from_request(request.GET)
 | 
			
		||||
    query = queries.query_archived_bookmarks(request.user, request.user_profile, search)
 | 
			
		||||
    return action(request, query)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -54,11 +54,12 @@ class BookmarkItem:
 | 
			
		||||
 | 
			
		||||
class BookmarkListContext:
 | 
			
		||||
    def __init__(self, request: WSGIRequest) -> None:
 | 
			
		||||
        self.request = request
 | 
			
		||||
        self.search = BookmarkSearch.from_request(self.request)
 | 
			
		||||
 | 
			
		||||
        user = request.user
 | 
			
		||||
        user_profile = request.user_profile
 | 
			
		||||
 | 
			
		||||
        self.request = request
 | 
			
		||||
        self.search = BookmarkSearch.from_request(self.request.GET, user_profile.search_preferences)
 | 
			
		||||
 | 
			
		||||
        query_set = self.get_bookmark_query_set()
 | 
			
		||||
        page_number = request.GET.get('page')
 | 
			
		||||
        paginator = Paginator(query_set, DEFAULT_PAGE_SIZE)
 | 
			
		||||
@@ -175,8 +176,10 @@ class TagGroup:
 | 
			
		||||
 | 
			
		||||
class TagCloudContext:
 | 
			
		||||
    def __init__(self, request: WSGIRequest) -> None:
 | 
			
		||||
        user_profile = request.user_profile
 | 
			
		||||
 | 
			
		||||
        self.request = request
 | 
			
		||||
        self.search = BookmarkSearch.from_request(self.request)
 | 
			
		||||
        self.search = BookmarkSearch.from_request(self.request.GET, user_profile.search_preferences)
 | 
			
		||||
 | 
			
		||||
        query_set = self.get_tag_query_set()
 | 
			
		||||
        tags = list(query_set)
 | 
			
		||||
@@ -196,7 +199,7 @@ class TagCloudContext:
 | 
			
		||||
        raise Exception(f'Must be implemented by subclass')
 | 
			
		||||
 | 
			
		||||
    def get_selected_tags(self, tags: List[Tag]):
 | 
			
		||||
        parsed_query = queries.parse_query_string(self.search.query)
 | 
			
		||||
        parsed_query = queries.parse_query_string(self.search.q)
 | 
			
		||||
        tag_names = parsed_query['tag_names']
 | 
			
		||||
        if self.request.user_profile.tag_search == UserProfile.TAG_SEARCH_LAX:
 | 
			
		||||
            tag_names = tag_names + parsed_query['search_terms']
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user