mirror of
				https://github.com/sissbruecker/linkding.git
				synced 2025-11-03 20:44:05 +01:00 
			
		
		
		
	Add option to share bookmarks publicly (#503)
* Make shared view public, add user profile fallback * Allow unauthenticated access to shared bookmarks API * Link shared bookmarks in unauthenticated layout * Add public sharing setting * Only show shared bookmarks link if there are publicly shared bookmarks * Disable public sharing if sharing is disabled * Show specific helper text when public sharing is enabled * Fix tests * Add more tests * Improve setting description
This commit is contained in:
		@@ -1,5 +1,6 @@
 | 
			
		||||
from rest_framework import viewsets, mixins, status
 | 
			
		||||
from rest_framework.decorators import action
 | 
			
		||||
from rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.routers import DefaultRouter
 | 
			
		||||
 | 
			
		||||
@@ -18,6 +19,17 @@ class BookmarkViewSet(viewsets.GenericViewSet,
 | 
			
		||||
                      mixins.DestroyModelMixin):
 | 
			
		||||
    serializer_class = BookmarkSerializer
 | 
			
		||||
 | 
			
		||||
    def get_permissions(self):
 | 
			
		||||
        # Allow unauthenticated access to shared bookmarks.
 | 
			
		||||
        # The shared action should still filter bookmarks so that
 | 
			
		||||
        # unauthenticated users only see bookmarks from users that have public
 | 
			
		||||
        # sharing explicitly enabled
 | 
			
		||||
        if self.action == 'shared':
 | 
			
		||||
            return [AllowAny()]
 | 
			
		||||
 | 
			
		||||
        # Otherwise use default permissions which should require authentication
 | 
			
		||||
        return super().get_permissions()
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        user = self.request.user
 | 
			
		||||
        # For list action, use query set that applies search and tag projections
 | 
			
		||||
@@ -45,7 +57,8 @@ class BookmarkViewSet(viewsets.GenericViewSet,
 | 
			
		||||
    def shared(self, request):
 | 
			
		||||
        filters = BookmarkFilters(request)
 | 
			
		||||
        user = User.objects.filter(username=filters.user).first()
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(user, request.user.profile, filters.query)
 | 
			
		||||
        public_only = not request.user.is_authenticated
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(user, request.user_profile, filters.query, public_only)
 | 
			
		||||
        page = self.paginate_queryset(query_set)
 | 
			
		||||
        serializer = self.get_serializer_class()
 | 
			
		||||
        data = serializer(page, many=True).data
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,25 @@
 | 
			
		||||
from bookmarks import queries
 | 
			
		||||
from bookmarks.models import Toast
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def toasts(request):
 | 
			
		||||
    user = request.user if hasattr(request, 'user') else None
 | 
			
		||||
    toast_messages = Toast.objects.filter(owner=user, acknowledged=False) if user and user.is_authenticated else []
 | 
			
		||||
    user = request.user
 | 
			
		||||
    toast_messages = Toast.objects.filter(owner=user, acknowledged=False) if user.is_authenticated else []
 | 
			
		||||
    has_toasts = len(toast_messages) > 0
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        'has_toasts': has_toasts,
 | 
			
		||||
        'toast_messages': toast_messages,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def public_shares(request):
 | 
			
		||||
    # Only check for public shares for anonymous users
 | 
			
		||||
    if not request.user.is_authenticated:
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, request.user_profile, '', True)
 | 
			
		||||
        has_public_shares = query_set.count() > 0
 | 
			
		||||
        return {
 | 
			
		||||
            'has_public_shares': has_public_shares,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    return {}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								bookmarks/e2e/e2e_test_settings_general.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								bookmarks/e2e/e2e_test_settings_general.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from playwright.sync_api import sync_playwright, expect
 | 
			
		||||
 | 
			
		||||
from bookmarks.e2e.helpers import LinkdingE2ETestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
 | 
			
		||||
    def test_should_only_enable_public_sharing_if_sharing_is_enabled(self):
 | 
			
		||||
        with sync_playwright() as p:
 | 
			
		||||
            browser = self.setup_browser(p)
 | 
			
		||||
            page = browser.new_page()
 | 
			
		||||
            page.goto(self.live_server_url + reverse('bookmarks:settings.general'))
 | 
			
		||||
 | 
			
		||||
            enable_sharing = page.get_by_label('Enable bookmark sharing')
 | 
			
		||||
            enable_sharing_label = page.get_by_text('Enable bookmark sharing')
 | 
			
		||||
            enable_public_sharing = page.get_by_label('Enable public bookmark sharing')
 | 
			
		||||
            enable_public_sharing_label = page.get_by_text('Enable public bookmark sharing')
 | 
			
		||||
 | 
			
		||||
            # Public sharing is disabled by default
 | 
			
		||||
            expect(enable_sharing).not_to_be_checked()
 | 
			
		||||
            expect(enable_public_sharing).not_to_be_checked()
 | 
			
		||||
            expect(enable_public_sharing).to_be_disabled()
 | 
			
		||||
 | 
			
		||||
            # Enable sharing
 | 
			
		||||
            enable_sharing_label.click()
 | 
			
		||||
            expect(enable_sharing).to_be_checked()
 | 
			
		||||
            expect(enable_public_sharing).not_to_be_checked()
 | 
			
		||||
            expect(enable_public_sharing).to_be_enabled()
 | 
			
		||||
 | 
			
		||||
            # Enable public sharing
 | 
			
		||||
            enable_public_sharing_label.click()
 | 
			
		||||
            expect(enable_public_sharing).to_be_checked()
 | 
			
		||||
            expect(enable_public_sharing).to_be_enabled()
 | 
			
		||||
 | 
			
		||||
            # Disable sharing
 | 
			
		||||
            enable_sharing_label.click()
 | 
			
		||||
            expect(enable_sharing).not_to_be_checked()
 | 
			
		||||
            expect(enable_public_sharing).not_to_be_checked()
 | 
			
		||||
            expect(enable_public_sharing).to_be_disabled()
 | 
			
		||||
@@ -1,6 +1,24 @@
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.auth.middleware import RemoteUserMiddleware
 | 
			
		||||
 | 
			
		||||
from bookmarks.models import UserProfile
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomRemoteUserMiddleware(RemoteUserMiddleware):
 | 
			
		||||
    header = settings.LD_AUTH_PROXY_USERNAME_HEADER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserProfileMiddleware:
 | 
			
		||||
    def __init__(self, get_response):
 | 
			
		||||
        self.get_response = get_response
 | 
			
		||||
 | 
			
		||||
    def __call__(self, request):
 | 
			
		||||
        if request.user.is_authenticated:
 | 
			
		||||
            request.user_profile = request.user.profile
 | 
			
		||||
        else:
 | 
			
		||||
            request.user_profile = UserProfile()
 | 
			
		||||
            request.user_profile.enable_favicons = True
 | 
			
		||||
 | 
			
		||||
        response = self.get_response(request)
 | 
			
		||||
 | 
			
		||||
        return response
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 4.1.9 on 2023-08-14 07:08
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('bookmarks', '0023_userprofile_permanent_notes'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='userprofile',
 | 
			
		||||
            name='enable_public_sharing',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -176,6 +176,7 @@ class UserProfile(models.Model):
 | 
			
		||||
    tag_search = models.CharField(max_length=10, choices=TAG_SEARCH_CHOICES, blank=False,
 | 
			
		||||
                                  default=TAG_SEARCH_STRICT)
 | 
			
		||||
    enable_sharing = models.BooleanField(default=False, null=False)
 | 
			
		||||
    enable_public_sharing = models.BooleanField(default=False, null=False)
 | 
			
		||||
    enable_favicons = models.BooleanField(default=False, null=False)
 | 
			
		||||
    display_url = models.BooleanField(default=False, null=False)
 | 
			
		||||
    permanent_notes = models.BooleanField(default=False, null=False)
 | 
			
		||||
@@ -185,7 +186,7 @@ class UserProfileForm(forms.ModelForm):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = UserProfile
 | 
			
		||||
        fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'tag_search',
 | 
			
		||||
                  'enable_sharing', 'enable_favicons', 'display_url', 'permanent_notes']
 | 
			
		||||
                  'enable_sharing', 'enable_public_sharing', 'enable_favicons', 'display_url', 'permanent_notes']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=get_user_model())
 | 
			
		||||
 
 | 
			
		||||
@@ -17,10 +17,13 @@ def query_archived_bookmarks(user: User, profile: UserProfile, query_string: str
 | 
			
		||||
        .filter(is_archived=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def query_shared_bookmarks(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet:
 | 
			
		||||
    return _base_bookmarks_query(user, profile, query_string) \
 | 
			
		||||
        .filter(shared=True) \
 | 
			
		||||
        .filter(owner__profile__enable_sharing=True)
 | 
			
		||||
def query_shared_bookmarks(user: Optional[User], profile: UserProfile, query_string: str,
 | 
			
		||||
                           public_only: bool) -> QuerySet:
 | 
			
		||||
    conditions = Q(shared=True) & Q(owner__profile__enable_sharing=True)
 | 
			
		||||
    if public_only:
 | 
			
		||||
        conditions = conditions & Q(owner__profile__enable_public_sharing=True)
 | 
			
		||||
 | 
			
		||||
    return _base_bookmarks_query(user, profile, query_string).filter(conditions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _base_bookmarks_query(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet:
 | 
			
		||||
@@ -85,16 +88,17 @@ def query_archived_bookmark_tags(user: User, profile: UserProfile, query_string:
 | 
			
		||||
    return query_set.distinct()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet:
 | 
			
		||||
    bookmarks_query = query_shared_bookmarks(user, profile, query_string)
 | 
			
		||||
def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, query_string: str,
 | 
			
		||||
                               public_only: bool) -> QuerySet:
 | 
			
		||||
    bookmarks_query = query_shared_bookmarks(user, profile, query_string, public_only)
 | 
			
		||||
 | 
			
		||||
    query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
 | 
			
		||||
 | 
			
		||||
    return query_set.distinct()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def query_shared_bookmark_users(profile: UserProfile, query_string: str) -> QuerySet:
 | 
			
		||||
    bookmarks_query = query_shared_bookmarks(None, profile, query_string)
 | 
			
		||||
def query_shared_bookmark_users(profile: UserProfile, query_string: str, public_only: bool) -> QuerySet:
 | 
			
		||||
    bookmarks_query = query_shared_bookmarks(None, profile, query_string, public_only)
 | 
			
		||||
 | 
			
		||||
    query_set = User.objects.filter(bookmark__in=bookmarks_query)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load shared %}
 | 
			
		||||
{% load pagination %}
 | 
			
		||||
<ul class="bookmark-list{% if  request.user.profile.permanent_notes %} show-notes{% endif %}">
 | 
			
		||||
<ul class="bookmark-list{% if  request.user_profile.permanent_notes %} show-notes{% endif %}">
 | 
			
		||||
  {% for bookmark in bookmarks %}
 | 
			
		||||
    <li data-is-bookmark-item>
 | 
			
		||||
      <label class="form-checkbox bulk-edit-toggle">
 | 
			
		||||
@@ -11,13 +11,13 @@
 | 
			
		||||
      <div class="title">
 | 
			
		||||
        <a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
 | 
			
		||||
           class="{% if bookmark.unread %}text-italic{% endif %}">
 | 
			
		||||
          {% if bookmark.favicon_file and request.user.profile.enable_favicons %}
 | 
			
		||||
          {% if bookmark.favicon_file and request.user_profile.enable_favicons %}
 | 
			
		||||
            <img src="{% static bookmark.favicon_file %}" alt="">
 | 
			
		||||
          {% endif %}
 | 
			
		||||
          {{ bookmark.resolved_title }}
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
      {% if request.user.profile.display_url %}
 | 
			
		||||
      {% if request.user_profile.display_url %}
 | 
			
		||||
        <div class="url-path truncate">
 | 
			
		||||
          <a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
 | 
			
		||||
             class="url-display text-sm">
 | 
			
		||||
@@ -46,7 +46,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      {% endif %}
 | 
			
		||||
      <div class="actions text-gray text-sm">
 | 
			
		||||
        {% if request.user.profile.bookmark_date_display == 'relative' %}
 | 
			
		||||
        {% if request.user_profile.bookmark_date_display == 'relative' %}
 | 
			
		||||
          <span>
 | 
			
		||||
              {% if bookmark.web_archive_snapshot_url %}
 | 
			
		||||
                <a href="{{ bookmark.web_archive_snapshot_url }}"
 | 
			
		||||
@@ -61,7 +61,7 @@
 | 
			
		||||
            </span>
 | 
			
		||||
          <span class="separator">|</span>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if request.user.profile.bookmark_date_display == 'absolute' %}
 | 
			
		||||
        {% if request.user_profile.bookmark_date_display == 'absolute' %}
 | 
			
		||||
          <span>
 | 
			
		||||
              {% if bookmark.web_archive_snapshot_url %}
 | 
			
		||||
                <a href="{{ bookmark.web_archive_snapshot_url }}"
 | 
			
		||||
@@ -103,7 +103,7 @@
 | 
			
		||||
              <a href="?{% replace_query_param user=bookmark.owner.username %}">{{ bookmark.owner.username }}</a>
 | 
			
		||||
            </span>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if bookmark.notes and not request.user.profile.permanent_notes %}
 | 
			
		||||
        {% if bookmark.notes and not request.user_profile.permanent_notes %}
 | 
			
		||||
          <span class="separator">|</span>
 | 
			
		||||
          <button class="btn btn-link btn-sm toggle-notes" title="Toggle notes">
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-notes" width="16" height="16"
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,7 @@
 | 
			
		||||
      Unread bookmarks can be filtered for, and marked as read after you had a chance to look at them.
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  {% if request.user.profile.enable_sharing %}
 | 
			
		||||
  {% if request.user_profile.enable_sharing %}
 | 
			
		||||
    <div class="form-group">
 | 
			
		||||
      <label for="{{ form.shared.id_for_label }}" class="form-checkbox">
 | 
			
		||||
        {{ form.shared }}
 | 
			
		||||
@@ -98,7 +98,11 @@
 | 
			
		||||
        <span>Share</span>
 | 
			
		||||
      </label>
 | 
			
		||||
      <div class="form-input-hint">
 | 
			
		||||
        Share this bookmark with other users.
 | 
			
		||||
        {% if request.user_profile.enable_public_sharing %}
 | 
			
		||||
          Share this bookmark with other registered users and anonymous users.
 | 
			
		||||
        {% else %}
 | 
			
		||||
          Share this bookmark with other registered users.
 | 
			
		||||
        {% endif %}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,9 @@
 | 
			
		||||
  <title>linkding</title>
 | 
			
		||||
  {# Include SASS styles, files are resolved from bookmarks/styles #}
 | 
			
		||||
  {# Include specific theme variant based on user profile setting #}
 | 
			
		||||
  {% if request.user.profile.theme == 'light' %}
 | 
			
		||||
  {% if request.user_profile.theme == 'light' %}
 | 
			
		||||
    <link href="{% sass_src 'theme-light.scss' %}" rel="stylesheet" type="text/css"/>
 | 
			
		||||
  {% elif request.user.profile.theme == 'dark' %}
 | 
			
		||||
  {% elif request.user_profile.theme == 'dark' %}
 | 
			
		||||
    <link href="{% sass_src 'theme-dark.scss' %}" rel="stylesheet" type="text/css"/>
 | 
			
		||||
  {% else %}
 | 
			
		||||
    {# Use auto theme as fallback #}
 | 
			
		||||
@@ -51,11 +51,16 @@
 | 
			
		||||
        <h1>linkding</h1>
 | 
			
		||||
      </a>
 | 
			
		||||
    </section>
 | 
			
		||||
    {# Only show nav items menu when logged in #}
 | 
			
		||||
    {% if request.user.is_authenticated %}
 | 
			
		||||
      {# Only show nav items menu when logged in #}
 | 
			
		||||
      <section class="navbar-section">
 | 
			
		||||
        {% include 'bookmarks/nav_menu.html' %}
 | 
			
		||||
      </section>
 | 
			
		||||
    {% elif has_public_shares %}
 | 
			
		||||
      {# Otherwise show link to shared bookmarks if there are publicly shared bookmarks #}
 | 
			
		||||
      <section class="navbar-section">
 | 
			
		||||
        <a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared bookmarks</a>
 | 
			
		||||
      </section>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
  </div>
 | 
			
		||||
</header>
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      {% if request.user.profile.enable_sharing %}
 | 
			
		||||
      {% if request.user_profile.enable_sharing %}
 | 
			
		||||
        <li>
 | 
			
		||||
          <a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
 | 
			
		||||
        </li>
 | 
			
		||||
@@ -59,7 +59,7 @@
 | 
			
		||||
      <li style="padding-left: 1rem">
 | 
			
		||||
        <a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      {% if request.user.profile.enable_sharing %}
 | 
			
		||||
      {% if request.user_profile.enable_sharing %}
 | 
			
		||||
        <li style="padding-left: 1rem">
 | 
			
		||||
          <a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
 | 
			
		||||
        </li>
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,8 @@
 | 
			
		||||
          <div class="form-input-hint">
 | 
			
		||||
            In strict mode, tags must be prefixed with a hash character (#).
 | 
			
		||||
            In lax mode, tags can also be searched without the hash character.
 | 
			
		||||
            Note that tags without the hash character are indistinguishable from search terms, which means the search result will also include bookmarks where a search term matches otherwise.
 | 
			
		||||
            Note that tags without the hash character are indistinguishable from search terms, which means the search
 | 
			
		||||
            result will also include bookmarks where a search term matches otherwise.
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
@@ -77,7 +78,7 @@
 | 
			
		||||
            documentation</a> on how to configure a custom favicon provider.
 | 
			
		||||
            Icons are downloaded in the background, and it may take a while for them to show up.
 | 
			
		||||
          </div>
 | 
			
		||||
          {% if request.user.profile.enable_favicons and enable_refresh_favicons %}
 | 
			
		||||
          {% if request.user_profile.enable_favicons and enable_refresh_favicons %}
 | 
			
		||||
            <button class="btn mt-2" name="refresh_favicons">Refresh Favicons</button>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
          {% if refresh_favicons_success_message %}
 | 
			
		||||
@@ -112,6 +113,17 @@
 | 
			
		||||
            Disabling this feature will hide all previously shared bookmarks from other users.
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
          <label for="{{ form.enable_public_sharing.id_for_label }}" class="form-checkbox">
 | 
			
		||||
            {{ form.enable_public_sharing }}
 | 
			
		||||
            <i class="form-icon"></i> Enable public bookmark sharing
 | 
			
		||||
          </label>
 | 
			
		||||
          <div class="form-input-hint">
 | 
			
		||||
            Makes shared bookmarks publicly accessible, without requiring a login.
 | 
			
		||||
            That means that anyone with a link to this instance can view shared bookmarks via the <a
 | 
			
		||||
              href="{% url 'bookmarks:shared' %}">shared bookmarks page</a>.
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
          <input type="submit" name="update_profile" value="Save" class="btn btn-primary mt-2">
 | 
			
		||||
          {% if update_profile_success_message %}
 | 
			
		||||
@@ -196,4 +208,22 @@
 | 
			
		||||
    </section>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <script>
 | 
			
		||||
    // Automatically disable public bookmark sharing if bookmark sharing is disabled
 | 
			
		||||
    const enableSharing = document.getElementById("{{ form.enable_sharing.id_for_label }}");
 | 
			
		||||
    const enablePublicSharing = document.getElementById("{{ form.enable_public_sharing.id_for_label }}");
 | 
			
		||||
 | 
			
		||||
    function updatePublicSharing() {
 | 
			
		||||
      if (enableSharing.checked) {
 | 
			
		||||
        enablePublicSharing.disabled = false;
 | 
			
		||||
      } else {
 | 
			
		||||
        enablePublicSharing.disabled = true;
 | 
			
		||||
        enablePublicSharing.checked = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updatePublicSharing();
 | 
			
		||||
    enableSharing.addEventListener("change", updatePublicSharing);
 | 
			
		||||
  </script>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ def remove_tag_from_query(context, tag_name: str):
 | 
			
		||||
        tag_name_with_hash = '#' + tag_name
 | 
			
		||||
        query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name_with_hash)]
 | 
			
		||||
        # When using lax tag search, also remove tag without hash
 | 
			
		||||
        profile = context.request.user.profile
 | 
			
		||||
        profile = context.request.user_profile
 | 
			
		||||
        if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
 | 
			
		||||
            query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name)]
 | 
			
		||||
        # Rebuild query string
 | 
			
		||||
 
 | 
			
		||||
@@ -76,11 +76,12 @@ class BookmarkFactoryMixin:
 | 
			
		||||
        tag.save()
 | 
			
		||||
        return tag
 | 
			
		||||
 | 
			
		||||
    def setup_user(self, name: str = None, enable_sharing: bool = False):
 | 
			
		||||
    def setup_user(self, name: str = None, enable_sharing: bool = False, enable_public_sharing: bool = False):
 | 
			
		||||
        if not name:
 | 
			
		||||
            name = get_random_string(length=32)
 | 
			
		||||
        user = User.objects.create_user(name, 'user@example.com', 'password123')
 | 
			
		||||
        user.profile.enable_sharing = enable_sharing
 | 
			
		||||
        user.profile.enable_public_sharing = enable_public_sharing
 | 
			
		||||
        user.profile.save()
 | 
			
		||||
        return user
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								bookmarks/tests/test_anonymous_view.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								bookmarks/tests/test_anonymous_view.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AnonymousViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def assertSharedBookmarksLinkCount(self, response, count):
 | 
			
		||||
        url = reverse('bookmarks:shared')
 | 
			
		||||
        self.assertContains(response, f'<a href="{url}" class="btn btn-link">Shared bookmarks</a>',
 | 
			
		||||
                            count=count)
 | 
			
		||||
 | 
			
		||||
    def test_publicly_shared_bookmarks_link(self):
 | 
			
		||||
        # should not render link if no public shares exist
 | 
			
		||||
        user = self.setup_user(enable_sharing=True)
 | 
			
		||||
        self.setup_bookmark(user=user, shared=True)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('login'))
 | 
			
		||||
        self.assertSharedBookmarksLinkCount(response, 0)
 | 
			
		||||
 | 
			
		||||
        # should render link if public shares exist
 | 
			
		||||
        user.profile.enable_public_sharing = True
 | 
			
		||||
        user.profile.save()
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('login'))
 | 
			
		||||
        self.assertSharedBookmarksLinkCount(response, 1)
 | 
			
		||||
@@ -75,7 +75,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            'placeholder=" " autofocus class="form-input" required '
 | 
			
		||||
            'id="id_url">',
 | 
			
		||||
            html)
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    def test_should_prefill_title_from_url_parameter(self):
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:new') + '?title=Example%20Title')
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
@@ -85,7 +85,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            'class="form-input" maxlength="512" autocomplete="off" '
 | 
			
		||||
            'id="id_title">',
 | 
			
		||||
            html)
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    def test_should_prefill_description_from_url_parameter(self):
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:new') + '?description=Example%20Site%20Description')
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
@@ -160,8 +160,32 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            </label>            
 | 
			
		||||
        ''', html, count=1)
 | 
			
		||||
 | 
			
		||||
    def test_should_hide_notes_if_there_are_no_notes(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
 | 
			
		||||
    def test_should_show_respective_share_hint(self):
 | 
			
		||||
        self.user.profile.enable_sharing = True
 | 
			
		||||
        self.user.profile.save()
 | 
			
		||||
 | 
			
		||||
        self.assertContains(response, '<details class="notes">', count=1)
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:new'))
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
        self.assertInHTML('''
 | 
			
		||||
          <div class="form-input-hint">
 | 
			
		||||
              Share this bookmark with other registered users.
 | 
			
		||||
          </div>
 | 
			
		||||
        ''', html)
 | 
			
		||||
 | 
			
		||||
        self.user.profile.enable_public_sharing = True
 | 
			
		||||
        self.user.profile.save()
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:new'))
 | 
			
		||||
        html = response.content.decode()
 | 
			
		||||
        self.assertInHTML('''
 | 
			
		||||
          <div class="form-input-hint">
 | 
			
		||||
              Share this bookmark with other registered users and anonymous users.
 | 
			
		||||
          </div>
 | 
			
		||||
        ''', html)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_should_hide_notes_if_there_are_no_notes(self):
 | 
			
		||||
    bookmark = self.setup_bookmark()
 | 
			
		||||
    response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
 | 
			
		||||
 | 
			
		||||
    self.assertContains(response, '<details class="notes">', count=1)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all()):
 | 
			
		||||
        rf = RequestFactory()
 | 
			
		||||
        request = rf.get(url)
 | 
			
		||||
        request.user = self.get_or_create_test_user()
 | 
			
		||||
        request.user_profile = self.get_or_create_test_user().profile
 | 
			
		||||
        filters = BookmarkFilters(request)
 | 
			
		||||
        context = RequestContext(request, {
 | 
			
		||||
            'request': request,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
 | 
			
		||||
 | 
			
		||||
class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
    def authenticate(self) -> None:
 | 
			
		||||
        user = self.get_or_create_test_user()
 | 
			
		||||
        self.client.force_login(user)
 | 
			
		||||
 | 
			
		||||
@@ -65,6 +65,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            ''', html, count=0)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user3 = self.setup_user(enable_sharing=True)
 | 
			
		||||
@@ -89,6 +90,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_shared_bookmarks_from_selected_user(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user3 = self.setup_user(enable_sharing=True)
 | 
			
		||||
@@ -108,6 +110,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
    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),
 | 
			
		||||
@@ -126,7 +129,29 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_only_publicly_shared_bookmarks_without_login(self):
 | 
			
		||||
        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),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
 | 
			
		||||
        self.assertContains(response, '<ul class="bookmark-list">')  # Should render list
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks)
 | 
			
		||||
        self.assertInvisibleBookmarks(response, invisible_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_tags_for_shared_bookmarks_from_all_users_that_have_sharing_enabled(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user3 = self.setup_user(enable_sharing=True)
 | 
			
		||||
@@ -158,6 +183,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertInvisibleTags(response, invisible_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_tags_for_shared_bookmarks_from_selected_user(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user3 = self.setup_user(enable_sharing=True)
 | 
			
		||||
@@ -180,6 +206,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertInvisibleTags(response, invisible_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_tags_for_bookmarks_matching_query(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user3 = self.setup_user(enable_sharing=True)
 | 
			
		||||
@@ -207,7 +234,32 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertVisibleTags(response, visible_tags)
 | 
			
		||||
        self.assertInvisibleTags(response, invisible_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_only_tags_for_publicly_shared_bookmarks_without_login(self):
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
 | 
			
		||||
        visible_tags = [
 | 
			
		||||
            self.setup_tag(user=user1),
 | 
			
		||||
            self.setup_tag(user=user1),
 | 
			
		||||
        ]
 | 
			
		||||
        invisible_tags = [
 | 
			
		||||
            self.setup_tag(user=user2),
 | 
			
		||||
            self.setup_tag(user=user2),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        self.setup_bookmark(shared=True, user=user1, tags=[visible_tags[0]])
 | 
			
		||||
        self.setup_bookmark(shared=True, user=user1, tags=[visible_tags[1]])
 | 
			
		||||
 | 
			
		||||
        self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
 | 
			
		||||
        self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
 | 
			
		||||
        self.assertVisibleTags(response, visible_tags)
 | 
			
		||||
        self.assertInvisibleTags(response, invisible_tags)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        expected_visible_users = [
 | 
			
		||||
            self.setup_user(enable_sharing=True),
 | 
			
		||||
            self.setup_user(enable_sharing=True),
 | 
			
		||||
@@ -226,30 +278,53 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertVisibleUserOptions(response, expected_visible_users)
 | 
			
		||||
        self.assertInvisibleUserOptions(response, expected_invisible_users)
 | 
			
		||||
 | 
			
		||||
    def test_should_list_only_users_with_publicly_shared_bookmarks_without_login(self):
 | 
			
		||||
        expected_visible_users = [
 | 
			
		||||
            self.setup_user(enable_sharing=True, enable_public_sharing=True),
 | 
			
		||||
            self.setup_user(enable_sharing=True, enable_public_sharing=True),
 | 
			
		||||
        ]
 | 
			
		||||
        self.setup_bookmark(shared=True, user=expected_visible_users[0])
 | 
			
		||||
        self.setup_bookmark(shared=True, user=expected_visible_users[1])
 | 
			
		||||
 | 
			
		||||
def test_should_open_bookmarks_in_new_page_by_default(self):
 | 
			
		||||
    visible_bookmarks = [
 | 
			
		||||
        self.setup_bookmark(shared=True),
 | 
			
		||||
        self.setup_bookmark(shared=True),
 | 
			
		||||
        self.setup_bookmark(shared=True)
 | 
			
		||||
    ]
 | 
			
		||||
        expected_invisible_users = [
 | 
			
		||||
            self.setup_user(enable_sharing=True),
 | 
			
		||||
            self.setup_user(enable_sharing=True),
 | 
			
		||||
        ]
 | 
			
		||||
        self.setup_bookmark(shared=True, user=expected_invisible_users[0])
 | 
			
		||||
        self.setup_bookmark(shared=True, user=expected_invisible_users[1])
 | 
			
		||||
 | 
			
		||||
    response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
        self.assertVisibleUserOptions(response, expected_visible_users)
 | 
			
		||||
        self.assertInvisibleUserOptions(response, expected_invisible_users)
 | 
			
		||||
 | 
			
		||||
    self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
 | 
			
		||||
    def test_should_open_bookmarks_in_new_page_by_default(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user = self.get_or_create_test_user()
 | 
			
		||||
        user.profile.enable_sharing = True
 | 
			
		||||
        user.profile.save()
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(shared=True),
 | 
			
		||||
            self.setup_bookmark(shared=True),
 | 
			
		||||
            self.setup_bookmark(shared=True)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
 | 
			
		||||
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
 | 
			
		||||
    user = self.get_or_create_test_user()
 | 
			
		||||
    user.profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
 | 
			
		||||
    user.profile.save()
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
 | 
			
		||||
 | 
			
		||||
    visible_bookmarks = [
 | 
			
		||||
        self.setup_bookmark(shared=True),
 | 
			
		||||
        self.setup_bookmark(shared=True),
 | 
			
		||||
        self.setup_bookmark(shared=True)
 | 
			
		||||
    ]
 | 
			
		||||
    def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        user = self.get_or_create_test_user()
 | 
			
		||||
        user.profile.enable_sharing = True
 | 
			
		||||
        user.profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
 | 
			
		||||
        user.profile.save()
 | 
			
		||||
 | 
			
		||||
    response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
        visible_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(shared=True),
 | 
			
		||||
            self.setup_bookmark(shared=True),
 | 
			
		||||
            self.setup_bookmark(shared=True)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
 | 
			
		||||
        response = self.client.get(reverse('bookmarks:shared'))
 | 
			
		||||
 | 
			
		||||
        self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,6 @@ from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
 | 
			
		||||
class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0]
 | 
			
		||||
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
 | 
			
		||||
        self.tag1 = self.setup_tag()
 | 
			
		||||
        self.tag2 = self.setup_tag()
 | 
			
		||||
        self.bookmark1 = self.setup_bookmark(tags=[self.tag1, self.tag2], notes='Test notes')
 | 
			
		||||
@@ -26,6 +24,10 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.archived_bookmark1 = self.setup_bookmark(is_archived=True, tags=[self.tag1, self.tag2])
 | 
			
		||||
        self.archived_bookmark2 = self.setup_bookmark(is_archived=True)
 | 
			
		||||
 | 
			
		||||
    def authenticate(self):
 | 
			
		||||
        self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0]
 | 
			
		||||
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
 | 
			
		||||
 | 
			
		||||
    def assertBookmarkListEqual(self, data_list, bookmarks):
 | 
			
		||||
        expectations = []
 | 
			
		||||
        for bookmark in bookmarks:
 | 
			
		||||
@@ -53,24 +55,34 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertCountEqual(data_list, expectations)
 | 
			
		||||
 | 
			
		||||
    def test_list_bookmarks(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], [self.bookmark1, self.bookmark2, self.bookmark3])
 | 
			
		||||
 | 
			
		||||
    def test_list_bookmarks_should_filter_by_query(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-list') + '?q=#' + self.tag1.name,
 | 
			
		||||
                            expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], [self.bookmark1])
 | 
			
		||||
 | 
			
		||||
    def test_list_archived_bookmarks_does_not_return_unarchived_bookmarks(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1, self.archived_bookmark2])
 | 
			
		||||
 | 
			
		||||
    def test_list_archived_bookmarks_should_filter_by_query(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-archived') + '?q=#' + self.tag1.name,
 | 
			
		||||
                            expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1])
 | 
			
		||||
 | 
			
		||||
    def test_list_shared_bookmarks(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user3 = self.setup_user(enable_sharing=True)
 | 
			
		||||
@@ -89,7 +101,23 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_list_only_publicly_shared_bookmarks_when_not_logged_in(self):
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
 | 
			
		||||
        shared_bookmarks = [
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user1),
 | 
			
		||||
            self.setup_bookmark(shared=True, user=user1)
 | 
			
		||||
        ]
 | 
			
		||||
        self.setup_bookmark(shared=True, user=user2)
 | 
			
		||||
        self.setup_bookmark(shared=True, user=user2)
 | 
			
		||||
 | 
			
		||||
        response = self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], shared_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_list_shared_bookmarks_should_filter_by_query_and_user(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        # Search by query
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
@@ -131,6 +159,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)
 | 
			
		||||
 | 
			
		||||
    def test_create_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            'url': 'https://example.com/',
 | 
			
		||||
            'title': 'Test title',
 | 
			
		||||
@@ -155,6 +185,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
 | 
			
		||||
 | 
			
		||||
    def test_create_bookmark_with_same_url_updates_existing_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        original_bookmark = self.setup_bookmark()
 | 
			
		||||
        data = {
 | 
			
		||||
            'url': original_bookmark.url,
 | 
			
		||||
@@ -182,6 +214,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
 | 
			
		||||
 | 
			
		||||
    def test_create_bookmark_replaces_whitespace_in_tag_names(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            'url': 'https://example.com/',
 | 
			
		||||
            'title': 'Test title',
 | 
			
		||||
@@ -194,10 +228,14 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertListEqual(tag_names, ['tag-1', 'tag-2'])
 | 
			
		||||
 | 
			
		||||
    def test_create_bookmark_minimal_payload(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/'}
 | 
			
		||||
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
 | 
			
		||||
 | 
			
		||||
    def test_create_archived_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            'url': 'https://example.com/',
 | 
			
		||||
            'title': 'Test title',
 | 
			
		||||
@@ -216,41 +254,55 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
 | 
			
		||||
 | 
			
		||||
    def test_create_bookmark_is_not_archived_by_default(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/'}
 | 
			
		||||
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
 | 
			
		||||
        bookmark = Bookmark.objects.get(url=data['url'])
 | 
			
		||||
        self.assertFalse(bookmark.is_archived)
 | 
			
		||||
 | 
			
		||||
    def test_create_unread_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/', 'unread': True}
 | 
			
		||||
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
 | 
			
		||||
        bookmark = Bookmark.objects.get(url=data['url'])
 | 
			
		||||
        self.assertTrue(bookmark.unread)
 | 
			
		||||
 | 
			
		||||
    def test_create_bookmark_is_not_unread_by_default(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/'}
 | 
			
		||||
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
 | 
			
		||||
        bookmark = Bookmark.objects.get(url=data['url'])
 | 
			
		||||
        self.assertFalse(bookmark.unread)
 | 
			
		||||
 | 
			
		||||
    def test_create_shared_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/', 'shared': True}
 | 
			
		||||
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
 | 
			
		||||
        bookmark = Bookmark.objects.get(url=data['url'])
 | 
			
		||||
        self.assertTrue(bookmark.shared)
 | 
			
		||||
 | 
			
		||||
    def test_create_bookmark_is_not_shared_by_default(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/'}
 | 
			
		||||
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
 | 
			
		||||
        bookmark = Bookmark.objects.get(url=data['url'])
 | 
			
		||||
        self.assertFalse(bookmark.shared)
 | 
			
		||||
 | 
			
		||||
    def test_get_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        response = self.get(url, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        self.assertBookmarkListEqual([response.data], [self.bookmark1])
 | 
			
		||||
 | 
			
		||||
    def test_update_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/'}
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
@@ -258,11 +310,15 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(updated_bookmark.url, data['url'])
 | 
			
		||||
 | 
			
		||||
    def test_update_bookmark_fails_without_required_fields(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'title': 'https://example.com/'}
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
    def test_update_bookmark_with_minimal_payload_clears_all_fields(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/'}
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
@@ -274,6 +330,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(updated_bookmark.tag_names, [])
 | 
			
		||||
 | 
			
		||||
    def test_update_bookmark_unread_flag(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/', 'unread': True}
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
@@ -281,6 +339,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(updated_bookmark.unread, True)
 | 
			
		||||
 | 
			
		||||
    def test_update_bookmark_shared_flag(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com/', 'shared': True}
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
@@ -288,6 +348,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(updated_bookmark.shared, True)
 | 
			
		||||
 | 
			
		||||
    def test_patch_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        data = {'url': 'https://example.com'}
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
@@ -344,6 +406,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertListEqual(tag_names, ['updated-tag-1', 'updated-tag-2'])
 | 
			
		||||
 | 
			
		||||
    def test_patch_with_empty_payload_does_not_modify_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        self.patch(url, {}, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
        updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id)
 | 
			
		||||
@@ -353,23 +417,31 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertListEqual(updated_bookmark.tag_names, self.bookmark1.tag_names)
 | 
			
		||||
 | 
			
		||||
    def test_delete_bookmark(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
 | 
			
		||||
        self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
        self.assertEqual(len(Bookmark.objects.filter(id=self.bookmark1.id)), 0)
 | 
			
		||||
 | 
			
		||||
    def test_archive(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        url = reverse('bookmarks:bookmark-archive', args=[self.bookmark1.id])
 | 
			
		||||
        self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
        bookmark = Bookmark.objects.get(id=self.bookmark1.id)
 | 
			
		||||
        self.assertTrue(bookmark.is_archived)
 | 
			
		||||
 | 
			
		||||
    def test_unarchive(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        url = reverse('bookmarks:bookmark-unarchive', args=[self.archived_bookmark1.id])
 | 
			
		||||
        self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
        bookmark = Bookmark.objects.get(id=self.archived_bookmark1.id)
 | 
			
		||||
        self.assertFalse(bookmark.is_archived)
 | 
			
		||||
 | 
			
		||||
    def test_check_returns_no_bookmark_if_url_is_not_bookmarked(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        url = reverse('bookmarks:bookmark-check')
 | 
			
		||||
        check_url = urllib.parse.quote_plus('https://example.com')
 | 
			
		||||
        response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
@@ -378,6 +450,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertIsNone(bookmark_data)
 | 
			
		||||
 | 
			
		||||
    def test_check_returns_scraped_metadata_if_url_is_not_bookmarked(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata:
 | 
			
		||||
            expected_metadata = WebsiteMetadata(
 | 
			
		||||
                'https://example.com',
 | 
			
		||||
@@ -397,6 +471,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
            self.assertIsNotNone(expected_metadata.description, metadata['description'])
 | 
			
		||||
 | 
			
		||||
    def test_check_returns_bookmark_if_url_is_bookmarked(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        bookmark = self.setup_bookmark(url='https://example.com',
 | 
			
		||||
                                       title='Example title',
 | 
			
		||||
                                       description='Example description')
 | 
			
		||||
@@ -413,6 +489,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(bookmark.description, bookmark_data['description'])
 | 
			
		||||
 | 
			
		||||
    def test_check_returns_existing_metadata_if_url_is_bookmarked(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        bookmark = self.setup_bookmark(url='https://example.com',
 | 
			
		||||
                                       website_title='Existing title',
 | 
			
		||||
                                       website_description='Existing description')
 | 
			
		||||
@@ -430,6 +508,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
            self.assertIsNotNone(bookmark.website_description, metadata['description'])
 | 
			
		||||
 | 
			
		||||
    def test_can_only_access_own_bookmarks(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
        other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
 | 
			
		||||
        inaccessible_bookmark = self.setup_bookmark(user=other_user)
 | 
			
		||||
        inaccessible_shared_bookmark = self.setup_bookmark(user=other_user, shared=True)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								bookmarks/tests/test_bookmarks_api_permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								bookmarks/tests/test_bookmarks_api_permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
import urllib.parse
 | 
			
		||||
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.authtoken.models import Token
 | 
			
		||||
 | 
			
		||||
from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BookmarksApiPermissionsTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def authenticate(self) -> None:
 | 
			
		||||
        self.api_token = Token.objects.get_or_create(user=self.get_or_create_test_user())[0]
 | 
			
		||||
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.api_token.key)
 | 
			
		||||
 | 
			
		||||
    def test_list_bookmarks_requires_authentication(self):
 | 
			
		||||
        self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
 | 
			
		||||
    def test_list_archived_bookmarks_requires_authentication(self):
 | 
			
		||||
        self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.get(reverse('bookmarks:bookmark-archived'), expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
 | 
			
		||||
    def test_list_shared_bookmarks_does_not_require_authentication(self):
 | 
			
		||||
        self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
 | 
			
		||||
    def test_create_bookmark_requires_authentication(self):
 | 
			
		||||
        data = {
 | 
			
		||||
            'url': 'https://example.com/',
 | 
			
		||||
            'title': 'Test title',
 | 
			
		||||
            'description': 'Test description',
 | 
			
		||||
            'notes': 'Test notes',
 | 
			
		||||
            'is_archived': False,
 | 
			
		||||
            'unread': False,
 | 
			
		||||
            'shared': False,
 | 
			
		||||
            'tag_names': ['tag1', 'tag2']
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
 | 
			
		||||
 | 
			
		||||
    def test_get_bookmark_requires_authentication(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
 | 
			
		||||
 | 
			
		||||
        self.get(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.get(url, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
 | 
			
		||||
    def test_update_bookmark_requires_authentication(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
        data = {'url': 'https://example.com/'}
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
 | 
			
		||||
 | 
			
		||||
        self.put(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.put(url, data, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
 | 
			
		||||
    def test_patch_bookmark_requires_authentication(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
        data = {'url': 'https://example.com'}
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
 | 
			
		||||
 | 
			
		||||
        self.patch(url, data, expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.patch(url, data, expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
 | 
			
		||||
    def test_delete_bookmark_requires_authentication(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
        url = reverse('bookmarks:bookmark-detail', args=[bookmark.id])
 | 
			
		||||
 | 
			
		||||
        self.delete(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
 | 
			
		||||
    def test_archive_requires_authentication(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
        url = reverse('bookmarks:bookmark-archive', args=[bookmark.id])
 | 
			
		||||
 | 
			
		||||
        self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
 | 
			
		||||
    def test_unarchive_requires_authentication(self):
 | 
			
		||||
        bookmark = self.setup_bookmark(is_archived=True)
 | 
			
		||||
        url = reverse('bookmarks:bookmark-unarchive', args=[bookmark.id])
 | 
			
		||||
 | 
			
		||||
        self.post(url, expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
 | 
			
		||||
 | 
			
		||||
    def test_check_requires_authentication(self):
 | 
			
		||||
        url = reverse('bookmarks:bookmark-check')
 | 
			
		||||
        check_url = urllib.parse.quote_plus('https://example.com')
 | 
			
		||||
 | 
			
		||||
        self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_401_UNAUTHORIZED)
 | 
			
		||||
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK)
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
from dateutil.relativedelta import relativedelta
 | 
			
		||||
from django.contrib.auth.models import AnonymousUser
 | 
			
		||||
from django.core.paginator import Paginator
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.template import Template, RequestContext
 | 
			
		||||
from django.test import TestCase, RequestFactory
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
@@ -7,17 +9,23 @@ from django.utils import timezone, formats
 | 
			
		||||
 | 
			
		||||
from bookmarks.models import Bookmark, UserProfile, User
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
 | 
			
		||||
from bookmarks.middlewares import UserProfileMiddleware
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
 | 
			
		||||
    def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank', unread: bool = False):
 | 
			
		||||
    def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank'):
 | 
			
		||||
        unread = bookmark.unread
 | 
			
		||||
        favicon_img = f'<img src="/static/{bookmark.favicon_file}" alt="">' if bookmark.favicon_file else ''
 | 
			
		||||
        self.assertInHTML(
 | 
			
		||||
            f'''
 | 
			
		||||
            <a href="{bookmark.url}" 
 | 
			
		||||
                target="{link_target}" 
 | 
			
		||||
                rel="noopener" 
 | 
			
		||||
                class="{'text-italic' if unread else ''}">{bookmark.resolved_title}</a>
 | 
			
		||||
                class="{'text-italic' if unread else ''}">
 | 
			
		||||
                {favicon_img}
 | 
			
		||||
                {bookmark.resolved_title}
 | 
			
		||||
            </a>
 | 
			
		||||
            ''',
 | 
			
		||||
            html
 | 
			
		||||
        )
 | 
			
		||||
@@ -130,22 +138,26 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
          </button>        
 | 
			
		||||
          ''', html, count=count)
 | 
			
		||||
 | 
			
		||||
    def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test') -> str:
 | 
			
		||||
    def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test',
 | 
			
		||||
                        user: User | AnonymousUser = None) -> str:
 | 
			
		||||
        rf = RequestFactory()
 | 
			
		||||
        request = rf.get(url)
 | 
			
		||||
        request.user = self.get_or_create_test_user()
 | 
			
		||||
        request.user = user or self.get_or_create_test_user()
 | 
			
		||||
        middleware = UserProfileMiddleware(lambda r: HttpResponse())
 | 
			
		||||
        middleware(request)
 | 
			
		||||
        paginator = Paginator(bookmarks, 10)
 | 
			
		||||
        page = paginator.page(1)
 | 
			
		||||
 | 
			
		||||
        context = RequestContext(request, {'bookmarks': page, 'return_url': '/test'})
 | 
			
		||||
        return template.render(context)
 | 
			
		||||
 | 
			
		||||
    def render_default_template(self, bookmarks: [Bookmark], url: str = '/test') -> str:
 | 
			
		||||
    def render_default_template(self, bookmarks: [Bookmark], url: str = '/test',
 | 
			
		||||
                                user: User | AnonymousUser = None) -> str:
 | 
			
		||||
        template = Template(
 | 
			
		||||
            '{% load bookmarks %}'
 | 
			
		||||
            '{% bookmark_list bookmarks return_url %}'
 | 
			
		||||
        )
 | 
			
		||||
        return self.render_template(bookmarks, template, url)
 | 
			
		||||
        return self.render_template(bookmarks, template, url, user)
 | 
			
		||||
 | 
			
		||||
    def render_template_with_link_target(self, bookmarks: [Bookmark], link_target: str) -> str:
 | 
			
		||||
        template = Template(
 | 
			
		||||
@@ -211,11 +223,11 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def test_bookmark_link_target_should_respect_unread_flag(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
        html = self.render_template_with_link_target([bookmark], '_self')
 | 
			
		||||
        self.assertBookmarksLink(html, bookmark, link_target='_self', unread=False)
 | 
			
		||||
        self.assertBookmarksLink(html, bookmark, link_target='_self')
 | 
			
		||||
 | 
			
		||||
        bookmark = self.setup_bookmark(unread=True)
 | 
			
		||||
        html = self.render_template_with_link_target([bookmark], '_self')
 | 
			
		||||
        self.assertBookmarksLink(html, bookmark, link_target='_self', unread=True)
 | 
			
		||||
        self.assertBookmarksLink(html, bookmark, link_target='_self')
 | 
			
		||||
 | 
			
		||||
    def test_web_archive_link_target_should_be_blank_by_default(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
@@ -402,3 +414,20 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        html = self.render_default_template([bookmark])
 | 
			
		||||
 | 
			
		||||
        self.assertNotesToggle(html, 0)
 | 
			
		||||
 | 
			
		||||
    def test_with_anonymous_user(self):
 | 
			
		||||
        bookmark = self.setup_bookmark()
 | 
			
		||||
        bookmark.date_added = timezone.now() - relativedelta(days=8)
 | 
			
		||||
        bookmark.web_archive_snapshot_url = 'https://web.archive.org/web/20230531200136/https://example.com'
 | 
			
		||||
        bookmark.notes = '**Example:** `print("Hello world!")`'
 | 
			
		||||
        bookmark.favicon_file = 'https_example_com.png'
 | 
			
		||||
        bookmark.save()
 | 
			
		||||
 | 
			
		||||
        html = self.render_default_template([bookmark], '/test', AnonymousUser())
 | 
			
		||||
        self.assertBookmarksLink(html, bookmark, link_target='_blank')
 | 
			
		||||
        self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank')
 | 
			
		||||
        self.assertNoBookmarkActions(html, bookmark)
 | 
			
		||||
        self.assertShareInfo(html, bookmark)
 | 
			
		||||
        note_html = '<p><strong>Example:</strong> <code>print("Hello world!")</code></p>'
 | 
			
		||||
        self.assertNotes(html, note_html, 1)
 | 
			
		||||
        self.assertFaviconVisible(html, bookmark)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,17 @@
 | 
			
		||||
from django.core.paginator import Paginator
 | 
			
		||||
from django.template import Template, RequestContext
 | 
			
		||||
from django.test import SimpleTestCase, RequestFactory
 | 
			
		||||
from django.test import TestCase, RequestFactory
 | 
			
		||||
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PaginationTagTest(SimpleTestCase):
 | 
			
		||||
class PaginationTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
 | 
			
		||||
    def render_template(self, num_items: int, page_size: int, current_page: int, url: str = '/test') -> 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
 | 
			
		||||
        paginator = Paginator(range(0, num_items), page_size)
 | 
			
		||||
        page = paginator.page(current_page)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -679,16 +679,26 @@ 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, '')
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, '', False)
 | 
			
		||||
        self.assertQueryResult(query_set, [shared_bookmarks])
 | 
			
		||||
 | 
			
		||||
        # Should respect search query
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, 'test title')
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, 'test title', False)
 | 
			
		||||
        self.assertQueryResult(query_set, [[shared_bookmarks[0]]])
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, '#' + tag.name)
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, '#' + tag.name, False)
 | 
			
		||||
        self.assertQueryResult(query_set, [[shared_bookmarks[2]]])
 | 
			
		||||
 | 
			
		||||
    def test_query_publicly_shared_bookmarks(self):
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
 | 
			
		||||
        bookmark1 = self.setup_bookmark(user=user1, shared=True)
 | 
			
		||||
        self.setup_bookmark(user=user2, shared=True)
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmarks(None, self.profile, '', True)
 | 
			
		||||
        self.assertQueryResult(query_set, [[bookmark1]])
 | 
			
		||||
 | 
			
		||||
    def test_query_shared_bookmark_tags(self):
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
@@ -710,10 +720,24 @@ 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, '')
 | 
			
		||||
        query_set = queries.query_shared_bookmark_tags(None, self.profile, '', False)
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query_set, [shared_tags])
 | 
			
		||||
 | 
			
		||||
    def test_query_publicly_shared_bookmark_tags(self):
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
 | 
			
		||||
        tag1 = self.setup_tag(user=user1)
 | 
			
		||||
        tag2 = self.setup_tag(user=user2)
 | 
			
		||||
 | 
			
		||||
        self.setup_bookmark(user=user1, shared=True, tags=[tag1]),
 | 
			
		||||
        self.setup_bookmark(user=user2, shared=True, tags=[tag2]),
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmark_tags(None, self.profile, '', True)
 | 
			
		||||
 | 
			
		||||
        self.assertQueryResult(query_set, [[tag1]])
 | 
			
		||||
 | 
			
		||||
    def test_query_shared_bookmark_users(self):
 | 
			
		||||
        users_with_shared_bookmarks = [
 | 
			
		||||
            self.setup_user(enable_sharing=True),
 | 
			
		||||
@@ -735,9 +759,19 @@ 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, '')
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, '', False)
 | 
			
		||||
        self.assertQueryResult(query_set, [users_with_shared_bookmarks])
 | 
			
		||||
 | 
			
		||||
        # Should respect search query
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, 'test title')
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, 'test title', False)
 | 
			
		||||
        self.assertQueryResult(query_set, [[users_with_shared_bookmarks[0]]])
 | 
			
		||||
 | 
			
		||||
    def test_query_publicly_shared_bookmark_users(self):
 | 
			
		||||
        user1 = self.setup_user(enable_sharing=True, enable_public_sharing=True)
 | 
			
		||||
        user2 = self.setup_user(enable_sharing=True)
 | 
			
		||||
 | 
			
		||||
        self.setup_bookmark(user=user1, shared=True)
 | 
			
		||||
        self.setup_bookmark(user=user2, shared=True)
 | 
			
		||||
 | 
			
		||||
        query_set = queries.query_shared_bookmark_users(self.profile, '', True)
 | 
			
		||||
        self.assertQueryResult(query_set, [[user1]])
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_BLANK,
 | 
			
		||||
            'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED,
 | 
			
		||||
            'enable_sharing': False,
 | 
			
		||||
            'enable_public_sharing': False,
 | 
			
		||||
            'enable_favicons': False,
 | 
			
		||||
            'tag_search': UserProfile.TAG_SEARCH_STRICT,
 | 
			
		||||
            'display_url': False,
 | 
			
		||||
@@ -54,6 +55,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
            'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_SELF,
 | 
			
		||||
            'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
 | 
			
		||||
            'enable_sharing': True,
 | 
			
		||||
            'enable_public_sharing': True,
 | 
			
		||||
            'enable_favicons': True,
 | 
			
		||||
            'tag_search': UserProfile.TAG_SEARCH_LAX,
 | 
			
		||||
            'display_url': True,
 | 
			
		||||
@@ -70,6 +72,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
        self.assertEqual(self.user.profile.bookmark_link_target, form_data['bookmark_link_target'])
 | 
			
		||||
        self.assertEqual(self.user.profile.web_archive_integration, form_data['web_archive_integration'])
 | 
			
		||||
        self.assertEqual(self.user.profile.enable_sharing, form_data['enable_sharing'])
 | 
			
		||||
        self.assertEqual(self.user.profile.enable_public_sharing, form_data['enable_public_sharing'])
 | 
			
		||||
        self.assertEqual(self.user.profile.enable_favicons, form_data['enable_favicons'])
 | 
			
		||||
        self.assertEqual(self.user.profile.tag_search, form_data['tag_search'])
 | 
			
		||||
        self.assertEqual(self.user.profile.display_url, form_data['display_url'])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,26 @@
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import User, AnonymousUser
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.template import Template, RequestContext
 | 
			
		||||
from django.test import TestCase, RequestFactory
 | 
			
		||||
 | 
			
		||||
from bookmarks.middlewares import UserProfileMiddleware
 | 
			
		||||
from bookmarks.models import Tag, UserProfile
 | 
			
		||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
    def render_template(self, tags: List[Tag], selected_tags: List[Tag] = None, url: str = '/test'):
 | 
			
		||||
    def render_template(self, tags: List[Tag], selected_tags: List[Tag] = None, url: str = '/test',
 | 
			
		||||
                        user: User | AnonymousUser = None):
 | 
			
		||||
        if not selected_tags:
 | 
			
		||||
            selected_tags = []
 | 
			
		||||
 | 
			
		||||
        rf = RequestFactory()
 | 
			
		||||
        request = rf.get(url)
 | 
			
		||||
        request.user = self.get_or_create_test_user()
 | 
			
		||||
        request.user = user or self.get_or_create_test_user()
 | 
			
		||||
        middleware = UserProfileMiddleware(lambda r: HttpResponse())
 | 
			
		||||
        middleware(request)
 | 
			
		||||
        context = RequestContext(request, {
 | 
			
		||||
            'request': request,
 | 
			
		||||
            'tags': tags,
 | 
			
		||||
@@ -209,3 +215,37 @@ class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
 | 
			
		||||
        self.assertTagGroups(rendered_template, [
 | 
			
		||||
            ['tag3', 'tag4', 'tag5']
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
    def test_with_anonymous_user(self):
 | 
			
		||||
        tags = [
 | 
			
		||||
            self.setup_tag(name='tag1'),
 | 
			
		||||
            self.setup_tag(name='tag2'),
 | 
			
		||||
            self.setup_tag(name='tag3'),
 | 
			
		||||
            self.setup_tag(name='tag4'),
 | 
			
		||||
            self.setup_tag(name='tag5'),
 | 
			
		||||
        ]
 | 
			
		||||
        selected_tags = [
 | 
			
		||||
            tags[0],
 | 
			
		||||
            tags[1],
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        rendered_template = self.render_template(tags, selected_tags, url='/test?q=%23tag1 %23tag2',
 | 
			
		||||
                                                 user=AnonymousUser())
 | 
			
		||||
 | 
			
		||||
        self.assertTagGroups(rendered_template, [
 | 
			
		||||
            ['tag3', 'tag4', 'tag5']
 | 
			
		||||
        ])
 | 
			
		||||
        self.assertNumSelectedTags(rendered_template, 2)
 | 
			
		||||
        self.assertInHTML('''
 | 
			
		||||
            <a href="?q=%23tag2"
 | 
			
		||||
               class="text-bold mr-2">
 | 
			
		||||
                <span>-tag1</span>
 | 
			
		||||
            </a>
 | 
			
		||||
        ''', rendered_template)
 | 
			
		||||
 | 
			
		||||
        self.assertInHTML('''
 | 
			
		||||
            <a href="?q=%23tag1"
 | 
			
		||||
               class="text-bold mr-2">
 | 
			
		||||
                <span>-tag2</span>
 | 
			
		||||
            </a>
 | 
			
		||||
        ''', rendered_template)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
 | 
			
		||||
    def render_template(self, url: str, users: QuerySet[User] = User.objects.all()):
 | 
			
		||||
        rf = RequestFactory()
 | 
			
		||||
        request = rf.get(url)
 | 
			
		||||
        request.user = self.get_or_create_test_user()
 | 
			
		||||
        request.user_profile = self.get_or_create_test_user().profile
 | 
			
		||||
        filters = BookmarkFilters(request)
 | 
			
		||||
        context = RequestContext(request, {
 | 
			
		||||
            'request': request,
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,8 @@ _default_page_size = 30
 | 
			
		||||
@login_required
 | 
			
		||||
def index(request):
 | 
			
		||||
    filters = BookmarkFilters(request)
 | 
			
		||||
    query_set = queries.query_bookmarks(request.user, request.user.profile, filters.query)
 | 
			
		||||
    tags = queries.query_bookmark_tags(request.user, request.user.profile, filters.query)
 | 
			
		||||
    query_set = queries.query_bookmarks(request.user, request.user_profile, filters.query)
 | 
			
		||||
    tags = queries.query_bookmark_tags(request.user, request.user_profile, filters.query)
 | 
			
		||||
    base_url = reverse('bookmarks:index')
 | 
			
		||||
    context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
 | 
			
		||||
    return render(request, 'bookmarks/index.html', context)
 | 
			
		||||
@@ -31,20 +31,20 @@ def index(request):
 | 
			
		||||
@login_required
 | 
			
		||||
def archived(request):
 | 
			
		||||
    filters = BookmarkFilters(request)
 | 
			
		||||
    query_set = queries.query_archived_bookmarks(request.user, request.user.profile, filters.query)
 | 
			
		||||
    tags = queries.query_archived_bookmark_tags(request.user, request.user.profile, filters.query)
 | 
			
		||||
    query_set = queries.query_archived_bookmarks(request.user, request.user_profile, filters.query)
 | 
			
		||||
    tags = queries.query_archived_bookmark_tags(request.user, request.user_profile, filters.query)
 | 
			
		||||
    base_url = reverse('bookmarks:archived')
 | 
			
		||||
    context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
 | 
			
		||||
    return render(request, 'bookmarks/archive.html', context)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
def shared(request):
 | 
			
		||||
    filters = BookmarkFilters(request)
 | 
			
		||||
    user = User.objects.filter(username=filters.user).first()
 | 
			
		||||
    query_set = queries.query_shared_bookmarks(user, request.user.profile, filters.query)
 | 
			
		||||
    tags = queries.query_shared_bookmark_tags(user, request.user.profile, filters.query)
 | 
			
		||||
    users = queries.query_shared_bookmark_users(request.user.profile, filters.query)
 | 
			
		||||
    public_only = not request.user.is_authenticated
 | 
			
		||||
    query_set = queries.query_shared_bookmarks(user, request.user_profile, filters.query, public_only)
 | 
			
		||||
    tags = queries.query_shared_bookmark_tags(user, request.user_profile, filters.query, public_only)
 | 
			
		||||
    users = queries.query_shared_bookmark_users(request.user_profile, filters.query, public_only)
 | 
			
		||||
    base_url = reverse('bookmarks:shared')
 | 
			
		||||
    context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
 | 
			
		||||
    context['users'] = users
 | 
			
		||||
@@ -70,11 +70,11 @@ def get_bookmark_view_context(request: WSGIRequest,
 | 
			
		||||
    paginator = Paginator(query_set, _default_page_size)
 | 
			
		||||
    bookmarks = paginator.get_page(page)
 | 
			
		||||
    tags = list(tags)
 | 
			
		||||
    selected_tags = _get_selected_tags(tags, filters.query, request.user.profile)
 | 
			
		||||
    selected_tags = _get_selected_tags(tags, filters.query, request.user_profile)
 | 
			
		||||
    # Prefetch related objects, this avoids n+1 queries when accessing fields in templates
 | 
			
		||||
    prefetch_related_objects(bookmarks.object_list, 'owner', 'tags')
 | 
			
		||||
    return_url = generate_return_url(base_url, page, filters)
 | 
			
		||||
    link_target = request.user.profile.bookmark_link_target
 | 
			
		||||
    link_target = request.user_profile.bookmark_link_target
 | 
			
		||||
 | 
			
		||||
    if request.GET.get('tag'):
 | 
			
		||||
        mod = request.GET.copy()
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ def general(request):
 | 
			
		||||
            refresh_favicons_success_message = 'Scheduled favicon update. This may take a while...'
 | 
			
		||||
 | 
			
		||||
    if not profile_form:
 | 
			
		||||
        profile_form = UserProfileForm(instance=request.user.profile)
 | 
			
		||||
        profile_form = UserProfileForm(instance=request.user_profile)
 | 
			
		||||
 | 
			
		||||
    return render(request, 'settings/general.html', {
 | 
			
		||||
        'form': profile_form,
 | 
			
		||||
@@ -141,7 +141,7 @@ def bookmark_import(request):
 | 
			
		||||
def bookmark_export(request):
 | 
			
		||||
    # noinspection PyBroadException
 | 
			
		||||
    try:
 | 
			
		||||
        bookmarks = list(query_bookmarks(request.user, request.user.profile, ''))
 | 
			
		||||
        bookmarks = list(query_bookmarks(request.user, request.user_profile, ''))
 | 
			
		||||
        # Prefetch tags to prevent n+1 queries
 | 
			
		||||
        prefetch_related_objects(bookmarks, 'tags')
 | 
			
		||||
        file_content = exporter.export_netscape_html(bookmarks)
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@ MIDDLEWARE = [
 | 
			
		||||
    'django.middleware.common.CommonMiddleware',
 | 
			
		||||
    'django.middleware.csrf.CsrfViewMiddleware',
 | 
			
		||||
    'django.contrib.auth.middleware.AuthenticationMiddleware',
 | 
			
		||||
    'bookmarks.middlewares.UserProfileMiddleware',
 | 
			
		||||
    'django.contrib.messages.middleware.MessageMiddleware',
 | 
			
		||||
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
 | 
			
		||||
    'django.middleware.locale.LocaleMiddleware',
 | 
			
		||||
@@ -71,6 +72,7 @@ TEMPLATES = [
 | 
			
		||||
                'django.contrib.auth.context_processors.auth',
 | 
			
		||||
                'django.contrib.messages.context_processors.messages',
 | 
			
		||||
                'bookmarks.context_processors.toasts',
 | 
			
		||||
                'bookmarks.context_processors.public_shares',
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user