mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-10 06:04:15 +00: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:
parent
22e8750c24
commit
ea240eefd9
29 changed files with 667 additions and 87 deletions
|
@ -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
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
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.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
|
||||||
|
|
|
@ -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,
|
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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
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 '
|
'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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
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 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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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]])
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue