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:
Sascha Ißbrücker 2023-08-15 00:20:52 +02:00 committed by GitHub
parent 22e8750c24
commit ea240eefd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 667 additions and 87 deletions

View file

@ -1,5 +1,6 @@
from rest_framework import viewsets, mixins, status from rest_framework import viewsets, mixins, status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
@ -18,6 +19,17 @@ class BookmarkViewSet(viewsets.GenericViewSet,
mixins.DestroyModelMixin): mixins.DestroyModelMixin):
serializer_class = BookmarkSerializer 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): def get_queryset(self):
user = self.request.user user = self.request.user
# For list action, use query set that applies search and tag projections # For list action, use query set that applies search and tag projections
@ -45,7 +57,8 @@ class BookmarkViewSet(viewsets.GenericViewSet,
def shared(self, request): def shared(self, request):
filters = BookmarkFilters(request) filters = BookmarkFilters(request)
user = User.objects.filter(username=filters.user).first() user = User.objects.filter(username=filters.user).first()
query_set = queries.query_shared_bookmarks(user, 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) page = self.paginate_queryset(query_set)
serializer = self.get_serializer_class() serializer = self.get_serializer_class()
data = serializer(page, many=True).data data = serializer(page, many=True).data

View file

@ -1,12 +1,25 @@
from bookmarks import queries
from bookmarks.models import Toast from bookmarks.models import Toast
def toasts(request): def toasts(request):
user = request.user if hasattr(request, 'user') else None user = request.user
toast_messages = Toast.objects.filter(owner=user, acknowledged=False) if user and user.is_authenticated else [] toast_messages = Toast.objects.filter(owner=user, acknowledged=False) if user.is_authenticated else []
has_toasts = len(toast_messages) > 0 has_toasts = len(toast_messages) > 0
return { return {
'has_toasts': has_toasts, 'has_toasts': has_toasts,
'toast_messages': toast_messages, '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 {}

View 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()

View file

@ -1,6 +1,24 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.middleware import RemoteUserMiddleware from django.contrib.auth.middleware import RemoteUserMiddleware
from bookmarks.models import UserProfile
class CustomRemoteUserMiddleware(RemoteUserMiddleware): class CustomRemoteUserMiddleware(RemoteUserMiddleware):
header = settings.LD_AUTH_PROXY_USERNAME_HEADER 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

View file

@ -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),
),
]

View file

@ -176,6 +176,7 @@ class UserProfile(models.Model):
tag_search = models.CharField(max_length=10, choices=TAG_SEARCH_CHOICES, blank=False, tag_search = models.CharField(max_length=10, choices=TAG_SEARCH_CHOICES, blank=False,
default=TAG_SEARCH_STRICT) default=TAG_SEARCH_STRICT)
enable_sharing = models.BooleanField(default=False, null=False) 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) enable_favicons = models.BooleanField(default=False, null=False)
display_url = models.BooleanField(default=False, null=False) display_url = models.BooleanField(default=False, null=False)
permanent_notes = models.BooleanField(default=False, null=False) permanent_notes = models.BooleanField(default=False, null=False)
@ -185,7 +186,7 @@ class UserProfileForm(forms.ModelForm):
class Meta: class Meta:
model = UserProfile model = UserProfile
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'tag_search', 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()) @receiver(post_save, sender=get_user_model())

View file

@ -17,10 +17,13 @@ def query_archived_bookmarks(user: User, profile: UserProfile, query_string: str
.filter(is_archived=True) .filter(is_archived=True)
def query_shared_bookmarks(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet: def query_shared_bookmarks(user: Optional[User], profile: UserProfile, query_string: str,
return _base_bookmarks_query(user, profile, query_string) \ public_only: bool) -> QuerySet:
.filter(shared=True) \ conditions = Q(shared=True) & Q(owner__profile__enable_sharing=True)
.filter(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: 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() return query_set.distinct()
def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, query_string: str) -> QuerySet: def query_shared_bookmark_tags(user: Optional[User], profile: UserProfile, query_string: str,
bookmarks_query = query_shared_bookmarks(user, profile, query_string) public_only: bool) -> QuerySet:
bookmarks_query = query_shared_bookmarks(user, profile, query_string, public_only)
query_set = Tag.objects.filter(bookmark__in=bookmarks_query) query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
return query_set.distinct() return query_set.distinct()
def query_shared_bookmark_users(profile: UserProfile, query_string: str) -> QuerySet: def query_shared_bookmark_users(profile: UserProfile, query_string: str, public_only: bool) -> QuerySet:
bookmarks_query = query_shared_bookmarks(None, profile, query_string) bookmarks_query = query_shared_bookmarks(None, profile, query_string, public_only)
query_set = User.objects.filter(bookmark__in=bookmarks_query) query_set = User.objects.filter(bookmark__in=bookmarks_query)

View file

@ -1,7 +1,7 @@
{% load static %} {% load static %}
{% load shared %} {% load shared %}
{% load pagination %} {% 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 %} {% for bookmark in bookmarks %}
<li data-is-bookmark-item> <li data-is-bookmark-item>
<label class="form-checkbox bulk-edit-toggle"> <label class="form-checkbox bulk-edit-toggle">
@ -11,13 +11,13 @@
<div class="title"> <div class="title">
<a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener" <a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
class="{% if bookmark.unread %}text-italic{% endif %}"> class="{% 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=""> <img src="{% static bookmark.favicon_file %}" alt="">
{% endif %} {% endif %}
{{ bookmark.resolved_title }} {{ bookmark.resolved_title }}
</a> </a>
</div> </div>
{% if request.user.profile.display_url %} {% if request.user_profile.display_url %}
<div class="url-path truncate"> <div class="url-path truncate">
<a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener" <a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener"
class="url-display text-sm"> class="url-display text-sm">
@ -46,7 +46,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="actions text-gray text-sm"> <div class="actions text-gray text-sm">
{% if request.user.profile.bookmark_date_display == 'relative' %} {% if request.user_profile.bookmark_date_display == 'relative' %}
<span> <span>
{% if bookmark.web_archive_snapshot_url %} {% if bookmark.web_archive_snapshot_url %}
<a href="{{ bookmark.web_archive_snapshot_url }}" <a href="{{ bookmark.web_archive_snapshot_url }}"
@ -61,7 +61,7 @@
</span> </span>
<span class="separator">|</span> <span class="separator">|</span>
{% endif %} {% endif %}
{% if request.user.profile.bookmark_date_display == 'absolute' %} {% if request.user_profile.bookmark_date_display == 'absolute' %}
<span> <span>
{% if bookmark.web_archive_snapshot_url %} {% if bookmark.web_archive_snapshot_url %}
<a href="{{ bookmark.web_archive_snapshot_url }}" <a href="{{ bookmark.web_archive_snapshot_url }}"
@ -103,7 +103,7 @@
<a href="?{% replace_query_param user=bookmark.owner.username %}">{{ bookmark.owner.username }}</a> <a href="?{% replace_query_param user=bookmark.owner.username %}">{{ bookmark.owner.username }}</a>
</span> </span>
{% endif %} {% 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> <span class="separator">|</span>
<button class="btn btn-link btn-sm toggle-notes" title="Toggle notes"> <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" <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-notes" width="16" height="16"

View file

@ -90,7 +90,7 @@
Unread bookmarks can be filtered for, and marked as read after you had a chance to look at them. Unread bookmarks can be filtered for, and marked as read after you had a chance to look at them.
</div> </div>
</div> </div>
{% if request.user.profile.enable_sharing %} {% if request.user_profile.enable_sharing %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.shared.id_for_label }}" class="form-checkbox"> <label for="{{ form.shared.id_for_label }}" class="form-checkbox">
{{ form.shared }} {{ form.shared }}
@ -98,7 +98,11 @@
<span>Share</span> <span>Share</span>
</label> </label>
<div class="form-input-hint"> <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>
</div> </div>
{% endif %} {% endif %}

View file

@ -17,9 +17,9 @@
<title>linkding</title> <title>linkding</title>
{# Include SASS styles, files are resolved from bookmarks/styles #} {# Include SASS styles, files are resolved from bookmarks/styles #}
{# Include specific theme variant based on user profile setting #} {# 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"/> <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"/> <link href="{% sass_src 'theme-dark.scss' %}" rel="stylesheet" type="text/css"/>
{% else %} {% else %}
{# Use auto theme as fallback #} {# Use auto theme as fallback #}
@ -51,11 +51,16 @@
<h1>linkding</h1> <h1>linkding</h1>
</a> </a>
</section> </section>
{# Only show nav items menu when logged in #}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{# Only show nav items menu when logged in #}
<section class="navbar-section"> <section class="navbar-section">
{% include 'bookmarks/nav_menu.html' %} {% include 'bookmarks/nav_menu.html' %}
</section> </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 %} {% endif %}
</div> </div>
</header> </header>

View file

@ -20,7 +20,7 @@
<li> <li>
<a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a> <a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
</li> </li>
{% if request.user.profile.enable_sharing %} {% if request.user_profile.enable_sharing %}
<li> <li>
<a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a> <a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
</li> </li>
@ -59,7 +59,7 @@
<li style="padding-left: 1rem"> <li style="padding-left: 1rem">
<a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a> <a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
</li> </li>
{% if request.user.profile.enable_sharing %} {% if request.user_profile.enable_sharing %}
<li style="padding-left: 1rem"> <li style="padding-left: 1rem">
<a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a> <a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
</li> </li>

View file

@ -61,7 +61,8 @@
<div class="form-input-hint"> <div class="form-input-hint">
In strict mode, tags must be prefixed with a hash character (#). In strict mode, tags must be prefixed with a hash character (#).
In lax mode, tags can also be searched without the 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> </div>
<div class="form-group"> <div class="form-group">
@ -77,7 +78,7 @@
documentation</a> on how to configure a custom favicon provider. 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. Icons are downloaded in the background, and it may take a while for them to show up.
</div> </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> <button class="btn mt-2" name="refresh_favicons">Refresh Favicons</button>
{% endif %} {% endif %}
{% if refresh_favicons_success_message %} {% if refresh_favicons_success_message %}
@ -112,6 +113,17 @@
Disabling this feature will hide all previously shared bookmarks from other users. Disabling this feature will hide all previously shared bookmarks from other users.
</div> </div>
</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"> <div class="form-group">
<input type="submit" name="update_profile" value="Save" class="btn btn-primary mt-2"> <input type="submit" name="update_profile" value="Save" class="btn btn-primary mt-2">
{% if update_profile_success_message %} {% if update_profile_success_message %}
@ -196,4 +208,22 @@
</section> </section>
</div> </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 %} {% endblock %}

View file

@ -49,7 +49,7 @@ def remove_tag_from_query(context, tag_name: str):
tag_name_with_hash = '#' + tag_name tag_name_with_hash = '#' + tag_name
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name_with_hash)] 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 # 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: if profile.tag_search == UserProfile.TAG_SEARCH_LAX:
query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name)] query_parts = [part for part in query_parts if str.lower(part) != str.lower(tag_name)]
# Rebuild query string # Rebuild query string

View file

@ -76,11 +76,12 @@ class BookmarkFactoryMixin:
tag.save() tag.save()
return tag 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: if not name:
name = get_random_string(length=32) name = get_random_string(length=32)
user = User.objects.create_user(name, 'user@example.com', 'password123') user = User.objects.create_user(name, 'user@example.com', 'password123')
user.profile.enable_sharing = enable_sharing user.profile.enable_sharing = enable_sharing
user.profile.enable_public_sharing = enable_public_sharing
user.profile.save() user.profile.save()
return user return user

View 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)

View file

@ -75,7 +75,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
'placeholder=" " autofocus class="form-input" required ' 'placeholder=" " autofocus class="form-input" required '
'id="id_url">', 'id="id_url">',
html) html)
def test_should_prefill_title_from_url_parameter(self): def test_should_prefill_title_from_url_parameter(self):
response = self.client.get(reverse('bookmarks:new') + '?title=Example%20Title') response = self.client.get(reverse('bookmarks:new') + '?title=Example%20Title')
html = response.content.decode() html = response.content.decode()
@ -85,7 +85,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
'class="form-input" maxlength="512" autocomplete="off" ' 'class="form-input" maxlength="512" autocomplete="off" '
'id="id_title">', 'id="id_title">',
html) html)
def test_should_prefill_description_from_url_parameter(self): def test_should_prefill_description_from_url_parameter(self):
response = self.client.get(reverse('bookmarks:new') + '?description=Example%20Site%20Description') response = self.client.get(reverse('bookmarks:new') + '?description=Example%20Site%20Description')
html = response.content.decode() html = response.content.decode()
@ -160,8 +160,32 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
</label> </label>
''', html, count=1) ''', html, count=1)
def test_should_hide_notes_if_there_are_no_notes(self): def test_should_show_respective_share_hint(self):
bookmark = self.setup_bookmark() self.user.profile.enable_sharing = True
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id])) 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)

View file

@ -10,6 +10,8 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin):
def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all()): def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all()):
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) 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) filters = BookmarkFilters(request)
context = RequestContext(request, { context = RequestContext(request, {
'request': request, 'request': request,

View file

@ -10,7 +10,7 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin): class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
def setUp(self) -> None: def authenticate(self) -> None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
self.client.force_login(user) self.client.force_login(user)
@ -65,6 +65,7 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
''', html, count=0) ''', html, count=0)
def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(self): def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
user3 = 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) self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_shared_bookmarks_from_selected_user(self): def test_should_list_shared_bookmarks_from_selected_user(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
user3 = 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) self.assertInvisibleBookmarks(response, invisible_bookmarks)
def test_should_list_bookmarks_matching_query(self): def test_should_list_bookmarks_matching_query(self):
self.authenticate()
user = self.setup_user(enable_sharing=True) user = self.setup_user(enable_sharing=True)
visible_bookmarks = [ visible_bookmarks = [
self.setup_bookmark(shared=True, title='searchvalue', user=user), self.setup_bookmark(shared=True, title='searchvalue', user=user),
@ -126,7 +129,29 @@ class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
self.assertVisibleBookmarks(response, visible_bookmarks) self.assertVisibleBookmarks(response, visible_bookmarks)
self.assertInvisibleBookmarks(response, invisible_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): 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) user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
user3 = 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) self.assertInvisibleTags(response, invisible_tags)
def test_should_list_tags_for_shared_bookmarks_from_selected_user(self): def test_should_list_tags_for_shared_bookmarks_from_selected_user(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
user3 = 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) self.assertInvisibleTags(response, invisible_tags)
def test_should_list_tags_for_bookmarks_matching_query(self): def test_should_list_tags_for_bookmarks_matching_query(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
user3 = 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.assertVisibleTags(response, visible_tags)
self.assertInvisibleTags(response, invisible_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): def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self):
self.authenticate()
expected_visible_users = [ expected_visible_users = [
self.setup_user(enable_sharing=True), self.setup_user(enable_sharing=True),
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.assertVisibleUserOptions(response, expected_visible_users)
self.assertInvisibleUserOptions(response, expected_invisible_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): expected_invisible_users = [
visible_bookmarks = [ self.setup_user(enable_sharing=True),
self.setup_bookmark(shared=True), self.setup_user(enable_sharing=True),
self.setup_bookmark(shared=True), ]
self.setup_bookmark(shared=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): self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
user = self.get_or_create_test_user()
user.profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
user.profile.save()
visible_bookmarks = [ def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
self.setup_bookmark(shared=True), self.authenticate()
self.setup_bookmark(shared=True), user = self.get_or_create_test_user()
self.setup_bookmark(shared=True) 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')

View file

@ -16,8 +16,6 @@ from bookmarks.tests.helpers import LinkdingApiTestCase, BookmarkFactoryMixin
class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin): class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
def setUp(self) -> None: 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.tag1 = self.setup_tag()
self.tag2 = self.setup_tag() self.tag2 = self.setup_tag()
self.bookmark1 = self.setup_bookmark(tags=[self.tag1, self.tag2], notes='Test notes') 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_bookmark1 = self.setup_bookmark(is_archived=True, tags=[self.tag1, self.tag2])
self.archived_bookmark2 = self.setup_bookmark(is_archived=True) 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): def assertBookmarkListEqual(self, data_list, bookmarks):
expectations = [] expectations = []
for bookmark in bookmarks: for bookmark in bookmarks:
@ -53,24 +55,34 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertCountEqual(data_list, expectations) self.assertCountEqual(data_list, expectations)
def test_list_bookmarks(self): def test_list_bookmarks(self):
self.authenticate()
response = self.get(reverse('bookmarks:bookmark-list'), expected_status_code=status.HTTP_200_OK) 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]) self.assertBookmarkListEqual(response.data['results'], [self.bookmark1, self.bookmark2, self.bookmark3])
def test_list_bookmarks_should_filter_by_query(self): def test_list_bookmarks_should_filter_by_query(self):
self.authenticate()
response = self.get(reverse('bookmarks:bookmark-list') + '?q=#' + self.tag1.name, response = self.get(reverse('bookmarks:bookmark-list') + '?q=#' + self.tag1.name,
expected_status_code=status.HTTP_200_OK) expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], [self.bookmark1]) self.assertBookmarkListEqual(response.data['results'], [self.bookmark1])
def test_list_archived_bookmarks_does_not_return_unarchived_bookmarks(self): 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) 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]) self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1, self.archived_bookmark2])
def test_list_archived_bookmarks_should_filter_by_query(self): def test_list_archived_bookmarks_should_filter_by_query(self):
self.authenticate()
response = self.get(reverse('bookmarks:bookmark-archived') + '?q=#' + self.tag1.name, response = self.get(reverse('bookmarks:bookmark-archived') + '?q=#' + self.tag1.name,
expected_status_code=status.HTTP_200_OK) expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1]) self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1])
def test_list_shared_bookmarks(self): def test_list_shared_bookmarks(self):
self.authenticate()
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = self.setup_user(enable_sharing=True) user2 = self.setup_user(enable_sharing=True)
user3 = 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) response = self.get(reverse('bookmarks:bookmark-shared'), expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual(response.data['results'], shared_bookmarks) 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): def test_list_shared_bookmarks_should_filter_by_query_and_user(self):
self.authenticate()
# Search by query # Search by query
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = 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) self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)
def test_create_bookmark(self): def test_create_bookmark(self):
self.authenticate()
data = { data = {
'url': 'https://example.com/', 'url': 'https://example.com/',
'title': 'Test title', 'title': 'Test title',
@ -155,6 +185,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1) self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
def test_create_bookmark_with_same_url_updates_existing_bookmark(self): def test_create_bookmark_with_same_url_updates_existing_bookmark(self):
self.authenticate()
original_bookmark = self.setup_bookmark() original_bookmark = self.setup_bookmark()
data = { data = {
'url': original_bookmark.url, 'url': original_bookmark.url,
@ -182,6 +214,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1) self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
def test_create_bookmark_replaces_whitespace_in_tag_names(self): def test_create_bookmark_replaces_whitespace_in_tag_names(self):
self.authenticate()
data = { data = {
'url': 'https://example.com/', 'url': 'https://example.com/',
'title': 'Test title', 'title': 'Test title',
@ -194,10 +228,14 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertListEqual(tag_names, ['tag-1', 'tag-2']) self.assertListEqual(tag_names, ['tag-1', 'tag-2'])
def test_create_bookmark_minimal_payload(self): def test_create_bookmark_minimal_payload(self):
self.authenticate()
data = {'url': 'https://example.com/'} data = {'url': 'https://example.com/'}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
def test_create_archived_bookmark(self): def test_create_archived_bookmark(self):
self.authenticate()
data = { data = {
'url': 'https://example.com/', 'url': 'https://example.com/',
'title': 'Test title', 'title': 'Test title',
@ -216,41 +254,55 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1) self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
def test_create_bookmark_is_not_archived_by_default(self): def test_create_bookmark_is_not_archived_by_default(self):
self.authenticate()
data = {'url': 'https://example.com/'} data = {'url': 'https://example.com/'}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url']) bookmark = Bookmark.objects.get(url=data['url'])
self.assertFalse(bookmark.is_archived) self.assertFalse(bookmark.is_archived)
def test_create_unread_bookmark(self): def test_create_unread_bookmark(self):
self.authenticate()
data = {'url': 'https://example.com/', 'unread': True} data = {'url': 'https://example.com/', 'unread': True}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url']) bookmark = Bookmark.objects.get(url=data['url'])
self.assertTrue(bookmark.unread) self.assertTrue(bookmark.unread)
def test_create_bookmark_is_not_unread_by_default(self): def test_create_bookmark_is_not_unread_by_default(self):
self.authenticate()
data = {'url': 'https://example.com/'} data = {'url': 'https://example.com/'}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url']) bookmark = Bookmark.objects.get(url=data['url'])
self.assertFalse(bookmark.unread) self.assertFalse(bookmark.unread)
def test_create_shared_bookmark(self): def test_create_shared_bookmark(self):
self.authenticate()
data = {'url': 'https://example.com/', 'shared': True} data = {'url': 'https://example.com/', 'shared': True}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url']) bookmark = Bookmark.objects.get(url=data['url'])
self.assertTrue(bookmark.shared) self.assertTrue(bookmark.shared)
def test_create_bookmark_is_not_shared_by_default(self): def test_create_bookmark_is_not_shared_by_default(self):
self.authenticate()
data = {'url': 'https://example.com/'} data = {'url': 'https://example.com/'}
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED) self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
bookmark = Bookmark.objects.get(url=data['url']) bookmark = Bookmark.objects.get(url=data['url'])
self.assertFalse(bookmark.shared) self.assertFalse(bookmark.shared)
def test_get_bookmark(self): def test_get_bookmark(self):
self.authenticate()
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
response = self.get(url, expected_status_code=status.HTTP_200_OK) response = self.get(url, expected_status_code=status.HTTP_200_OK)
self.assertBookmarkListEqual([response.data], [self.bookmark1]) self.assertBookmarkListEqual([response.data], [self.bookmark1])
def test_update_bookmark(self): def test_update_bookmark(self):
self.authenticate()
data = {'url': 'https://example.com/'} data = {'url': 'https://example.com/'}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK) 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']) self.assertEqual(updated_bookmark.url, data['url'])
def test_update_bookmark_fails_without_required_fields(self): def test_update_bookmark_fails_without_required_fields(self):
self.authenticate()
data = {'title': 'https://example.com/'} data = {'title': 'https://example.com/'}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST) self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)
def test_update_bookmark_with_minimal_payload_clears_all_fields(self): def test_update_bookmark_with_minimal_payload_clears_all_fields(self):
self.authenticate()
data = {'url': 'https://example.com/'} data = {'url': 'https://example.com/'}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK) 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, []) self.assertEqual(updated_bookmark.tag_names, [])
def test_update_bookmark_unread_flag(self): def test_update_bookmark_unread_flag(self):
self.authenticate()
data = {'url': 'https://example.com/', 'unread': True} data = {'url': 'https://example.com/', 'unread': True}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK) 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) self.assertEqual(updated_bookmark.unread, True)
def test_update_bookmark_shared_flag(self): def test_update_bookmark_shared_flag(self):
self.authenticate()
data = {'url': 'https://example.com/', 'shared': True} data = {'url': 'https://example.com/', 'shared': True}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.put(url, data, expected_status_code=status.HTTP_200_OK) 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) self.assertEqual(updated_bookmark.shared, True)
def test_patch_bookmark(self): def test_patch_bookmark(self):
self.authenticate()
data = {'url': 'https://example.com'} data = {'url': 'https://example.com'}
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.patch(url, data, expected_status_code=status.HTTP_200_OK) self.patch(url, data, expected_status_code=status.HTTP_200_OK)
@ -344,6 +406,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertListEqual(tag_names, ['updated-tag-1', 'updated-tag-2']) self.assertListEqual(tag_names, ['updated-tag-1', 'updated-tag-2'])
def test_patch_with_empty_payload_does_not_modify_bookmark(self): def test_patch_with_empty_payload_does_not_modify_bookmark(self):
self.authenticate()
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.patch(url, {}, expected_status_code=status.HTTP_200_OK) self.patch(url, {}, expected_status_code=status.HTTP_200_OK)
updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id) 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) self.assertListEqual(updated_bookmark.tag_names, self.bookmark1.tag_names)
def test_delete_bookmark(self): def test_delete_bookmark(self):
self.authenticate()
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT) self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
self.assertEqual(len(Bookmark.objects.filter(id=self.bookmark1.id)), 0) self.assertEqual(len(Bookmark.objects.filter(id=self.bookmark1.id)), 0)
def test_archive(self): def test_archive(self):
self.authenticate()
url = reverse('bookmarks:bookmark-archive', args=[self.bookmark1.id]) url = reverse('bookmarks:bookmark-archive', args=[self.bookmark1.id])
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT) self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
bookmark = Bookmark.objects.get(id=self.bookmark1.id) bookmark = Bookmark.objects.get(id=self.bookmark1.id)
self.assertTrue(bookmark.is_archived) self.assertTrue(bookmark.is_archived)
def test_unarchive(self): def test_unarchive(self):
self.authenticate()
url = reverse('bookmarks:bookmark-unarchive', args=[self.archived_bookmark1.id]) url = reverse('bookmarks:bookmark-unarchive', args=[self.archived_bookmark1.id])
self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT) self.post(url, expected_status_code=status.HTTP_204_NO_CONTENT)
bookmark = Bookmark.objects.get(id=self.archived_bookmark1.id) bookmark = Bookmark.objects.get(id=self.archived_bookmark1.id)
self.assertFalse(bookmark.is_archived) self.assertFalse(bookmark.is_archived)
def test_check_returns_no_bookmark_if_url_is_not_bookmarked(self): def test_check_returns_no_bookmark_if_url_is_not_bookmarked(self):
self.authenticate()
url = reverse('bookmarks:bookmark-check') url = reverse('bookmarks:bookmark-check')
check_url = urllib.parse.quote_plus('https://example.com') check_url = urllib.parse.quote_plus('https://example.com')
response = self.get(f'{url}?url={check_url}', expected_status_code=status.HTTP_200_OK) 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) self.assertIsNone(bookmark_data)
def test_check_returns_scraped_metadata_if_url_is_not_bookmarked(self): 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: with patch.object(website_loader, 'load_website_metadata') as mock_load_website_metadata:
expected_metadata = WebsiteMetadata( expected_metadata = WebsiteMetadata(
'https://example.com', 'https://example.com',
@ -397,6 +471,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertIsNotNone(expected_metadata.description, metadata['description']) self.assertIsNotNone(expected_metadata.description, metadata['description'])
def test_check_returns_bookmark_if_url_is_bookmarked(self): def test_check_returns_bookmark_if_url_is_bookmarked(self):
self.authenticate()
bookmark = self.setup_bookmark(url='https://example.com', bookmark = self.setup_bookmark(url='https://example.com',
title='Example title', title='Example title',
description='Example description') description='Example description')
@ -413,6 +489,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertEqual(bookmark.description, bookmark_data['description']) self.assertEqual(bookmark.description, bookmark_data['description'])
def test_check_returns_existing_metadata_if_url_is_bookmarked(self): def test_check_returns_existing_metadata_if_url_is_bookmarked(self):
self.authenticate()
bookmark = self.setup_bookmark(url='https://example.com', bookmark = self.setup_bookmark(url='https://example.com',
website_title='Existing title', website_title='Existing title',
website_description='Existing description') website_description='Existing description')
@ -430,6 +508,8 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
self.assertIsNotNone(bookmark.website_description, metadata['description']) self.assertIsNotNone(bookmark.website_description, metadata['description'])
def test_can_only_access_own_bookmarks(self): def test_can_only_access_own_bookmarks(self):
self.authenticate()
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123') other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
inaccessible_bookmark = self.setup_bookmark(user=other_user) inaccessible_bookmark = self.setup_bookmark(user=other_user)
inaccessible_shared_bookmark = self.setup_bookmark(user=other_user, shared=True) inaccessible_shared_bookmark = self.setup_bookmark(user=other_user, shared=True)

View 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)

View file

@ -1,5 +1,7 @@
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.contrib.auth.models import AnonymousUser
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import HttpResponse
from django.template import Template, RequestContext from django.template import Template, RequestContext
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from django.urls import reverse from django.urls import reverse
@ -7,17 +9,23 @@ from django.utils import timezone, formats
from bookmarks.models import Bookmark, UserProfile, User from bookmarks.models import Bookmark, UserProfile, User
from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.tests.helpers import BookmarkFactoryMixin
from bookmarks.middlewares import UserProfileMiddleware
class BookmarkListTagTest(TestCase, BookmarkFactoryMixin): 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( self.assertInHTML(
f''' f'''
<a href="{bookmark.url}" <a href="{bookmark.url}"
target="{link_target}" target="{link_target}"
rel="noopener" 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 html
) )
@ -130,22 +138,26 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
</button> </button>
''', html, count=count) ''', 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() rf = RequestFactory()
request = rf.get(url) 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) paginator = Paginator(bookmarks, 10)
page = paginator.page(1) page = paginator.page(1)
context = RequestContext(request, {'bookmarks': page, 'return_url': '/test'}) context = RequestContext(request, {'bookmarks': page, 'return_url': '/test'})
return template.render(context) 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( template = Template(
'{% load bookmarks %}' '{% load bookmarks %}'
'{% bookmark_list bookmarks return_url %}' '{% 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: def render_template_with_link_target(self, bookmarks: [Bookmark], link_target: str) -> str:
template = Template( template = Template(
@ -211,11 +223,11 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
def test_bookmark_link_target_should_respect_unread_flag(self): def test_bookmark_link_target_should_respect_unread_flag(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
html = self.render_template_with_link_target([bookmark], '_self') 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) bookmark = self.setup_bookmark(unread=True)
html = self.render_template_with_link_target([bookmark], '_self') 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): def test_web_archive_link_target_should_be_blank_by_default(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()
@ -402,3 +414,20 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
html = self.render_default_template([bookmark]) html = self.render_default_template([bookmark])
self.assertNotesToggle(html, 0) 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)

View file

@ -1,13 +1,17 @@
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.template import Template, RequestContext 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: def render_template(self, num_items: int, page_size: int, current_page: int, url: str = '/test') -> str:
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) 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) paginator = Paginator(range(0, num_items), page_size)
page = paginator.page(current_page) page = paginator.page(current_page)

View file

@ -679,16 +679,26 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
self.setup_bookmark(user=user4, shared=True, tags=[tag]), self.setup_bookmark(user=user4, shared=True, tags=[tag]),
# Should return shared bookmarks from all users # Should return shared bookmarks from all users
query_set = queries.query_shared_bookmarks(None, self.profile, '') query_set = queries.query_shared_bookmarks(None, self.profile, '', False)
self.assertQueryResult(query_set, [shared_bookmarks]) self.assertQueryResult(query_set, [shared_bookmarks])
# Should respect search query # Should respect search query
query_set = queries.query_shared_bookmarks(None, self.profile, 'test title') query_set = queries.query_shared_bookmarks(None, self.profile, 'test title', False)
self.assertQueryResult(query_set, [[shared_bookmarks[0]]]) 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]]]) 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): def test_query_shared_bookmark_tags(self):
user1 = self.setup_user(enable_sharing=True) user1 = self.setup_user(enable_sharing=True)
user2 = 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=user3, shared=False, tags=[self.setup_tag(user=user3)]),
self.setup_bookmark(user=user4, shared=True, tags=[self.setup_tag(user=user4)]), self.setup_bookmark(user=user4, shared=True, tags=[self.setup_tag(user=user4)]),
query_set = queries.query_shared_bookmark_tags(None, self.profile, '') query_set = queries.query_shared_bookmark_tags(None, self.profile, '', False)
self.assertQueryResult(query_set, [shared_tags]) 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): def test_query_shared_bookmark_users(self):
users_with_shared_bookmarks = [ users_with_shared_bookmarks = [
self.setup_user(enable_sharing=True), 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), self.setup_bookmark(user=users_without_shared_bookmarks[2], shared=True),
# Should return users with shared bookmarks # Should return users with shared bookmarks
query_set = queries.query_shared_bookmark_users(self.profile, '') query_set = queries.query_shared_bookmark_users(self.profile, '', False)
self.assertQueryResult(query_set, [users_with_shared_bookmarks]) self.assertQueryResult(query_set, [users_with_shared_bookmarks])
# Should respect search query # Should respect search query
query_set = queries.query_shared_bookmark_users(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]]]) 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]])

View file

@ -27,6 +27,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_BLANK, 'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_BLANK,
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED, 'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_DISABLED,
'enable_sharing': False, 'enable_sharing': False,
'enable_public_sharing': False,
'enable_favicons': False, 'enable_favicons': False,
'tag_search': UserProfile.TAG_SEARCH_STRICT, 'tag_search': UserProfile.TAG_SEARCH_STRICT,
'display_url': False, 'display_url': False,
@ -54,6 +55,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_SELF, 'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_SELF,
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED, 'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
'enable_sharing': True, 'enable_sharing': True,
'enable_public_sharing': True,
'enable_favicons': True, 'enable_favicons': True,
'tag_search': UserProfile.TAG_SEARCH_LAX, 'tag_search': UserProfile.TAG_SEARCH_LAX,
'display_url': True, '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.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.web_archive_integration, form_data['web_archive_integration'])
self.assertEqual(self.user.profile.enable_sharing, form_data['enable_sharing']) self.assertEqual(self.user.profile.enable_sharing, form_data['enable_sharing'])
self.assertEqual(self.user.profile.enable_public_sharing, form_data['enable_public_sharing'])
self.assertEqual(self.user.profile.enable_favicons, form_data['enable_favicons']) self.assertEqual(self.user.profile.enable_favicons, form_data['enable_favicons'])
self.assertEqual(self.user.profile.tag_search, form_data['tag_search']) self.assertEqual(self.user.profile.tag_search, form_data['tag_search'])
self.assertEqual(self.user.profile.display_url, form_data['display_url']) self.assertEqual(self.user.profile.display_url, form_data['display_url'])

View file

@ -1,20 +1,26 @@
from typing import List 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.template import Template, RequestContext
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from bookmarks.middlewares import UserProfileMiddleware
from bookmarks.models import Tag, UserProfile from bookmarks.models import Tag, UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
class TagCloudTagTest(TestCase, 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: if not selected_tags:
selected_tags = [] selected_tags = []
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) 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, { context = RequestContext(request, {
'request': request, 'request': request,
'tags': tags, 'tags': tags,
@ -209,3 +215,37 @@ class TagCloudTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
self.assertTagGroups(rendered_template, [ self.assertTagGroups(rendered_template, [
['tag3', 'tag4', 'tag5'] ['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)

View file

@ -10,6 +10,8 @@ class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
def render_template(self, url: str, users: QuerySet[User] = User.objects.all()): def render_template(self, url: str, users: QuerySet[User] = User.objects.all()):
rf = RequestFactory() rf = RequestFactory()
request = rf.get(url) 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) filters = BookmarkFilters(request)
context = RequestContext(request, { context = RequestContext(request, {
'request': request, 'request': request,

View file

@ -21,8 +21,8 @@ _default_page_size = 30
@login_required @login_required
def index(request): def index(request):
filters = BookmarkFilters(request) filters = BookmarkFilters(request)
query_set = queries.query_bookmarks(request.user, 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) tags = queries.query_bookmark_tags(request.user, request.user_profile, filters.query)
base_url = reverse('bookmarks:index') base_url = reverse('bookmarks:index')
context = get_bookmark_view_context(request, filters, query_set, tags, base_url) context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
return render(request, 'bookmarks/index.html', context) return render(request, 'bookmarks/index.html', context)
@ -31,20 +31,20 @@ def index(request):
@login_required @login_required
def archived(request): def archived(request):
filters = BookmarkFilters(request) filters = BookmarkFilters(request)
query_set = queries.query_archived_bookmarks(request.user, 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) tags = queries.query_archived_bookmark_tags(request.user, request.user_profile, filters.query)
base_url = reverse('bookmarks:archived') base_url = reverse('bookmarks:archived')
context = get_bookmark_view_context(request, filters, query_set, tags, base_url) context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
return render(request, 'bookmarks/archive.html', context) return render(request, 'bookmarks/archive.html', context)
@login_required
def shared(request): def shared(request):
filters = BookmarkFilters(request) filters = BookmarkFilters(request)
user = User.objects.filter(username=filters.user).first() user = User.objects.filter(username=filters.user).first()
query_set = queries.query_shared_bookmarks(user, request.user.profile, filters.query) public_only = not request.user.is_authenticated
tags = queries.query_shared_bookmark_tags(user, request.user.profile, filters.query) query_set = queries.query_shared_bookmarks(user, request.user_profile, filters.query, public_only)
users = queries.query_shared_bookmark_users(request.user.profile, filters.query) 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') base_url = reverse('bookmarks:shared')
context = get_bookmark_view_context(request, filters, query_set, tags, base_url) context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
context['users'] = users context['users'] = users
@ -70,11 +70,11 @@ def get_bookmark_view_context(request: WSGIRequest,
paginator = Paginator(query_set, _default_page_size) paginator = Paginator(query_set, _default_page_size)
bookmarks = paginator.get_page(page) bookmarks = paginator.get_page(page)
tags = list(tags) 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, this avoids n+1 queries when accessing fields in templates
prefetch_related_objects(bookmarks.object_list, 'owner', 'tags') prefetch_related_objects(bookmarks.object_list, 'owner', 'tags')
return_url = generate_return_url(base_url, page, filters) return_url = generate_return_url(base_url, page, filters)
link_target = request.user.profile.bookmark_link_target link_target = request.user_profile.bookmark_link_target
if request.GET.get('tag'): if request.GET.get('tag'):
mod = request.GET.copy() mod = request.GET.copy()

View file

@ -46,7 +46,7 @@ def general(request):
refresh_favicons_success_message = 'Scheduled favicon update. This may take a while...' refresh_favicons_success_message = 'Scheduled favicon update. This may take a while...'
if not profile_form: if not profile_form:
profile_form = UserProfileForm(instance=request.user.profile) profile_form = UserProfileForm(instance=request.user_profile)
return render(request, 'settings/general.html', { return render(request, 'settings/general.html', {
'form': profile_form, 'form': profile_form,
@ -141,7 +141,7 @@ def bookmark_import(request):
def bookmark_export(request): def bookmark_export(request):
# noinspection PyBroadException # noinspection PyBroadException
try: try:
bookmarks = list(query_bookmarks(request.user, request.user.profile, '')) bookmarks = list(query_bookmarks(request.user, request.user_profile, ''))
# Prefetch tags to prevent n+1 queries # Prefetch tags to prevent n+1 queries
prefetch_related_objects(bookmarks, 'tags') prefetch_related_objects(bookmarks, 'tags')
file_content = exporter.export_netscape_html(bookmarks) file_content = exporter.export_netscape_html(bookmarks)

View file

@ -52,6 +52,7 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'bookmarks.middlewares.UserProfileMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
@ -71,6 +72,7 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'bookmarks.context_processors.toasts', 'bookmarks.context_processors.toasts',
'bookmarks.context_processors.public_shares',
], ],
}, },
}, },