mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-10 06:04:15 +00:00
Add bookmark sharing (#311)
* Allow marking bookmarks as shared * Add basic share view * Ensure tag names in tag cloud are unique * Filter shared bookmarks by user * Add link for filtering by user * Prevent n+1 queries when rendering bookmark list * Prevent empty query params in return URL * Fix user select template tag name * Create shared bookmarks through API * List shared bookmarks through API * Show bookmark suggestions for shared view * Show unique tags in search suggestions * Sort user options * Add bookmark sharing feature flag * Add test for share setting default * Simplify settings view
This commit is contained in:
parent
e6718be53b
commit
fec966f687
40 changed files with 1358 additions and 74 deletions
|
@ -6,7 +6,7 @@ from rest_framework.routers import DefaultRouter
|
|||
|
||||
from bookmarks import queries
|
||||
from bookmarks.api.serializers import BookmarkSerializer, TagSerializer
|
||||
from bookmarks.models import Bookmark, Tag
|
||||
from bookmarks.models import Bookmark, BookmarkFilters, Tag, User
|
||||
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
|
||||
from bookmarks.services.website_loader import load_website_metadata
|
||||
|
||||
|
@ -42,6 +42,16 @@ class BookmarkViewSet(viewsets.GenericViewSet,
|
|||
data = serializer(page, many=True).data
|
||||
return self.get_paginated_response(data)
|
||||
|
||||
@action(methods=['get'], detail=False)
|
||||
def shared(self, request):
|
||||
filters = BookmarkFilters(request)
|
||||
user = User.objects.filter(username=filters.user).first()
|
||||
query_set = queries.query_shared_bookmarks(user, filters.query)
|
||||
page = self.paginate_queryset(query_set)
|
||||
serializer = self.get_serializer_class()
|
||||
data = serializer(page, many=True).data
|
||||
return self.get_paginated_response(data)
|
||||
|
||||
@action(methods=['post'], detail=True)
|
||||
def archive(self, request, pk):
|
||||
bookmark = self.get_object()
|
||||
|
|
|
@ -21,6 +21,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||
'website_description',
|
||||
'is_archived',
|
||||
'unread',
|
||||
'shared',
|
||||
'tag_names',
|
||||
'date_added',
|
||||
'date_modified'
|
||||
|
@ -37,6 +38,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||
description = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
is_archived = serializers.BooleanField(required=False, default=False)
|
||||
unread = serializers.BooleanField(required=False, default=False)
|
||||
shared = serializers.BooleanField(required=False, default=False)
|
||||
# Override readonly tag_names property to allow passing a list of tag names to create/update
|
||||
tag_names = TagListField(required=False, default=[])
|
||||
|
||||
|
@ -47,12 +49,13 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||
bookmark.description = validated_data['description']
|
||||
bookmark.is_archived = validated_data['is_archived']
|
||||
bookmark.unread = validated_data['unread']
|
||||
bookmark.shared = validated_data['shared']
|
||||
tag_string = build_tag_string(validated_data['tag_names'])
|
||||
return create_bookmark(bookmark, tag_string, self.context['user'])
|
||||
|
||||
def update(self, instance: Bookmark, validated_data):
|
||||
# Update fields if they were provided in the payload
|
||||
for key in ['url', 'title', 'description', 'unread']:
|
||||
for key in ['url', 'title', 'description', 'unread', 'shared']:
|
||||
if key in validated_data:
|
||||
setattr(instance, key, validated_data[key])
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
export let placeholder;
|
||||
export let value;
|
||||
export let tags;
|
||||
export let mode = 'default';
|
||||
export let mode = '';
|
||||
export let apiClient;
|
||||
export let filters;
|
||||
|
||||
let isFocus = false;
|
||||
let isOpen = false;
|
||||
|
@ -112,9 +113,12 @@
|
|||
let bookmarks = []
|
||||
|
||||
if (value && value.length >= 3) {
|
||||
const fetchedBookmarks = mode === 'archive'
|
||||
? await apiClient.getArchivedBookmarks(value, {limit: 5, offset: 0})
|
||||
: await apiClient.getBookmarks(value, {limit: 5, offset: 0})
|
||||
const path = mode ? `/${mode}` : ''
|
||||
const suggestionFilters = {
|
||||
...filters,
|
||||
q: value
|
||||
}
|
||||
const fetchedBookmarks = await apiClient.listBookmarks(suggestionFilters, {limit: 5, offset: 0, path})
|
||||
bookmarks = fetchedBookmarks.map(bookmark => {
|
||||
const fullLabel = bookmark.title || bookmark.website_title || bookmark.url
|
||||
const label = clampText(fullLabel, 60)
|
||||
|
|
|
@ -3,18 +3,19 @@ export class ApiClient {
|
|||
this.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
getBookmarks(query, options = {limit: 100, offset: 0}) {
|
||||
const encodedQuery = encodeURIComponent(query)
|
||||
const url = `${this.baseUrl}bookmarks/?q=${encodedQuery}&limit=${options.limit}&offset=${options.offset}`
|
||||
|
||||
return fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => data.results)
|
||||
}
|
||||
|
||||
getArchivedBookmarks(query, options = {limit: 100, offset: 0}) {
|
||||
const encodedQuery = encodeURIComponent(query)
|
||||
const url = `${this.baseUrl}bookmarks/archived/?q=${encodedQuery}&limit=${options.limit}&offset=${options.offset}`
|
||||
listBookmarks(filters, options = {limit: 100, offset: 0, path: ''}) {
|
||||
const query = [
|
||||
`limit=${options.limit}`,
|
||||
`offset=${options.offset}`,
|
||||
]
|
||||
Object.keys(filters).forEach(key => {
|
||||
const value = filters[key]
|
||||
if (value) {
|
||||
query.push(`${key}=${encodeURIComponent(value)}`)
|
||||
}
|
||||
})
|
||||
const queryString = query.join('&')
|
||||
const url = `${this.baseUrl}bookmarks${options.path}/?${queryString}`
|
||||
|
||||
return fetch(url)
|
||||
.then(response => response.json())
|
||||
|
|
18
bookmarks/migrations/0016_bookmark_shared.py
Normal file
18
bookmarks/migrations/0016_bookmark_shared.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.14 on 2022-08-02 18:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0015_feedtoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bookmark',
|
||||
name='shared',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
18
bookmarks/migrations/0017_userprofile_enable_sharing.py
Normal file
18
bookmarks/migrations/0017_userprofile_enable_sharing.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.14 on 2022-08-04 09:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0016_bookmark_shared'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='enable_sharing',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -5,6 +5,7 @@ from typing import List
|
|||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
@ -54,6 +55,7 @@ class Bookmark(models.Model):
|
|||
web_archive_snapshot_url = models.CharField(max_length=2048, blank=True)
|
||||
unread = models.BooleanField(default=False)
|
||||
is_archived = models.BooleanField(default=False)
|
||||
shared = models.BooleanField(default=False)
|
||||
date_added = models.DateTimeField()
|
||||
date_modified = models.DateTimeField()
|
||||
date_accessed = models.DateTimeField(blank=True, null=True)
|
||||
|
@ -100,12 +102,19 @@ class BookmarkForm(forms.ModelForm):
|
|||
description = forms.CharField(required=False,
|
||||
widget=forms.Textarea())
|
||||
unread = forms.BooleanField(required=False)
|
||||
shared = forms.BooleanField(required=False)
|
||||
# Hidden field that determines whether to close window/tab after saving the bookmark
|
||||
auto_close = forms.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
fields = ['url', 'tag_string', 'title', 'description', 'unread', 'auto_close']
|
||||
fields = ['url', 'tag_string', 'title', 'description', 'unread', 'shared', 'auto_close']
|
||||
|
||||
|
||||
class BookmarkFilters:
|
||||
def __init__(self, request: WSGIRequest):
|
||||
self.query = request.GET.get('q') or ''
|
||||
self.user = request.GET.get('user') or ''
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
|
@ -145,12 +154,13 @@ class UserProfile(models.Model):
|
|||
default=BOOKMARK_LINK_TARGET_BLANK)
|
||||
web_archive_integration = models.CharField(max_length=10, choices=WEB_ARCHIVE_INTEGRATION_CHOICES, blank=False,
|
||||
default=WEB_ARCHIVE_INTEGRATION_DISABLED)
|
||||
enable_sharing = models.BooleanField(default=False, null=False)
|
||||
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration']
|
||||
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target', 'web_archive_integration', 'enable_sharing']
|
||||
|
||||
|
||||
@receiver(post_save, sender=get_user_model())
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import Optional
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q, Count, Aggregate, CharField, Value, BooleanField, QuerySet
|
||||
|
||||
|
@ -27,7 +29,13 @@ def query_archived_bookmarks(user: User, query_string: str) -> QuerySet:
|
|||
.filter(is_archived=True)
|
||||
|
||||
|
||||
def _base_bookmarks_query(user: User, query_string: str) -> QuerySet:
|
||||
def query_shared_bookmarks(user: Optional[User], query_string: str) -> QuerySet:
|
||||
return _base_bookmarks_query(user, query_string) \
|
||||
.filter(shared=True) \
|
||||
.filter(owner__profile__enable_sharing=True)
|
||||
|
||||
|
||||
def _base_bookmarks_query(user: Optional[User], query_string: str) -> QuerySet:
|
||||
# Add aggregated tag info to bookmark instances
|
||||
query_set = Bookmark.objects \
|
||||
.annotate(tag_count=Count('tags'),
|
||||
|
@ -35,7 +43,8 @@ def _base_bookmarks_query(user: User, query_string: str) -> QuerySet:
|
|||
tag_projection=Value(True, BooleanField()))
|
||||
|
||||
# Filter for user
|
||||
query_set = query_set.filter(owner=user)
|
||||
if user:
|
||||
query_set = query_set.filter(owner=user)
|
||||
|
||||
# Split query into search terms and tags
|
||||
query = _parse_query_string(query_string)
|
||||
|
@ -88,6 +97,22 @@ def query_archived_bookmark_tags(user: User, query_string: str) -> QuerySet:
|
|||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_shared_bookmark_tags(user: Optional[User], query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_shared_bookmarks(user, query_string)
|
||||
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_shared_bookmark_users(query_string: str) -> QuerySet:
|
||||
bookmarks_query = query_shared_bookmarks(None, query_string)
|
||||
|
||||
query_set = User.objects.filter(bookmark__in=bookmarks_query)
|
||||
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def get_user_tags(user: User):
|
||||
return Tag.objects.filter(owner=user).all()
|
||||
|
||||
|
|
|
@ -117,6 +117,7 @@ def _merge_bookmark_data(from_bookmark: Bookmark, to_bookmark: Bookmark):
|
|||
to_bookmark.title = from_bookmark.title
|
||||
to_bookmark.description = from_bookmark.description
|
||||
to_bookmark.unread = from_bookmark.unread
|
||||
to_bookmark.shared = from_bookmark.shared
|
||||
|
||||
|
||||
def _update_website_metadata(bookmark: Bookmark):
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="content-area-header">
|
||||
<h2>Archived bookmarks</h2>
|
||||
<div class="spacer"></div>
|
||||
{% bookmark_search query tags mode='archive' %}
|
||||
{% bookmark_search filters tags mode='archived' %}
|
||||
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -54,21 +54,29 @@
|
|||
</span>
|
||||
<span class="text-gray text-sm">|</span>
|
||||
{% endif %}
|
||||
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}"
|
||||
class="btn btn-link btn-sm">Edit</a>
|
||||
{% if bookmark.is_archived %}
|
||||
<button type="submit" name="unarchive" value="{{ bookmark.id }}"
|
||||
class="btn btn-link btn-sm">Unarchive</button>
|
||||
{% if bookmark.owner == request.user %}
|
||||
{# Bookmark owner actions #}
|
||||
<a href="{% url 'bookmarks:edit' bookmark.id %}?return_url={{ return_url }}"
|
||||
class="btn btn-link btn-sm">Edit</a>
|
||||
{% if bookmark.is_archived %}
|
||||
<button type="submit" name="unarchive" value="{{ bookmark.id }}"
|
||||
class="btn btn-link btn-sm">Unarchive</button>
|
||||
{% else %}
|
||||
<button type="submit" name="archive" value="{{ bookmark.id }}"
|
||||
class="btn btn-link btn-sm">Archive</button>
|
||||
{% endif %}
|
||||
<button type="submit" name="remove" value="{{ bookmark.id }}"
|
||||
class="btn btn-link btn-sm btn-confirmation">Remove</button>
|
||||
{% if bookmark.unread %}
|
||||
<span class="text-gray text-sm">|</span>
|
||||
<button type="submit" name="mark_as_read" value="{{ bookmark.id }}"
|
||||
class="btn btn-link btn-sm">Mark as read</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="submit" name="archive" value="{{ bookmark.id }}"
|
||||
class="btn btn-link btn-sm">Archive</button>
|
||||
{% endif %}
|
||||
<button type="submit" name="remove" value="{{ bookmark.id }}"
|
||||
class="btn btn-link btn-sm btn-confirmation">Remove</button>
|
||||
{% if bookmark.unread %}
|
||||
<span class="text-gray text-sm">|</span>
|
||||
<button type="submit" name="mark_as_read" value="{{ bookmark.id }}"
|
||||
class="btn btn-link btn-sm">Mark as read</button>
|
||||
{# Shared bookmark actions #}
|
||||
<span class="text-gray text-sm">Shared by
|
||||
<a class="text-gray" href="?{% replace_query_param user=bookmark.owner.username %}">{{ bookmark.owner.username }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -75,6 +75,18 @@
|
|||
Unread bookmarks can be filtered for, and marked as read after you had a chance to look at them.
|
||||
</div>
|
||||
</div>
|
||||
{% if request.user.profile.enable_sharing %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.shared.id_for_label }}" class="form-checkbox">
|
||||
{{ form.shared }}
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
Share this bookmark with other users.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<br/>
|
||||
<div class="form-group">
|
||||
{% if auto_close %}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="content-area-header">
|
||||
<h2>Bookmarks</h2>
|
||||
<div class="spacer"></div>
|
||||
{% bookmark_search query tags %}
|
||||
{% bookmark_search filters tags %}
|
||||
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -15,6 +15,11 @@
|
|||
<li>
|
||||
<a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
|
||||
</li>
|
||||
{% if request.user.profile.enable_sharing %}
|
||||
<li>
|
||||
<a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{% url 'bookmarks:index' %}?q=!unread" class="btn btn-link">Unread</a>
|
||||
</li>
|
||||
|
@ -47,6 +52,11 @@
|
|||
<li style="padding-left: 1rem">
|
||||
<a href="{% url 'bookmarks:archived' %}" class="btn btn-link">Archived</a>
|
||||
</li>
|
||||
{% if request.user.profile.enable_sharing %}
|
||||
<li style="padding-left: 1rem">
|
||||
<a href="{% url 'bookmarks:shared' %}" class="btn btn-link">Shared</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li style="padding-left: 1rem">
|
||||
<a href="{% url 'bookmarks:index' %}?q=!unread" class="btn btn-link">Unread</a>
|
||||
</li>
|
||||
|
|
|
@ -3,10 +3,13 @@
|
|||
<div class="input-group">
|
||||
<span id="search-input-wrap">
|
||||
<input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
|
||||
value="{{ query }}">
|
||||
value="{{ filters.query }}">
|
||||
</span>
|
||||
<input type="submit" value="Search" class="btn input-group-btn">
|
||||
</div>
|
||||
{% if filters.user %}
|
||||
<input type="hidden" name="user" value="{{ filters.user }}">
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
@ -15,6 +18,11 @@
|
|||
window.addEventListener("load", function() {
|
||||
const currentTagsString = '{{ tags_string }}';
|
||||
const currentTags = currentTagsString.split(' ');
|
||||
const uniqueTags = [...new Set(currentTags)]
|
||||
const filters = {
|
||||
q: '{{ filters.query }}',
|
||||
user: '{{ filters.user }}',
|
||||
}
|
||||
const apiClient = new linkding.ApiClient('{% url 'bookmarks:api-root' %}')
|
||||
const wrapper = document.getElementById('search-input-wrap')
|
||||
const newWrapper = document.createElement('div')
|
||||
|
@ -23,10 +31,11 @@
|
|||
props: {
|
||||
name: 'q',
|
||||
placeholder: 'Search for words or #tags',
|
||||
value: '{{ query }}',
|
||||
tags: currentTags,
|
||||
value: '{{ filters.query }}',
|
||||
tags: uniqueTags,
|
||||
mode: '{{ mode }}',
|
||||
apiClient
|
||||
apiClient,
|
||||
filters,
|
||||
}
|
||||
})
|
||||
wrapper.parentElement.replaceChild(newWrapper, wrapper)
|
||||
|
|
48
bookmarks/templates/bookmarks/shared.html
Normal file
48
bookmarks/templates/bookmarks/shared.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{% extends "bookmarks/layout.html" %}
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
{% load bookmarks %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="bookmarks-page columns">
|
||||
|
||||
{# Bookmark list #}
|
||||
<section class="content-area column col-8 col-md-12">
|
||||
<div class="content-area-header">
|
||||
<h2>Shared bookmarks</h2>
|
||||
<div class="spacer"></div>
|
||||
{% bookmark_search filters tags mode='shared' %}
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions" action="{% url 'bookmarks:action' %}?return_url={{ return_url }}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if empty %}
|
||||
{% include 'bookmarks/empty_bookmarks.html' %}
|
||||
{% else %}
|
||||
{% bookmark_list bookmarks return_url link_target %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{# Filters #}
|
||||
<section class="content-area column col-4 hide-md">
|
||||
<div class="content-area-header">
|
||||
<h2>User</h2>
|
||||
</div>
|
||||
<div>
|
||||
{% user_select filters users %}
|
||||
<br>
|
||||
</div>
|
||||
<div class="content-area-header">
|
||||
<h2>Tags</h2>
|
||||
</div>
|
||||
{% tag_cloud tags %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="{% static "bundle.js" %}"></script>
|
||||
<script src="{% static "shared.js" %}"></script>
|
||||
{% endblock %}
|
29
bookmarks/templates/bookmarks/user_select.html
Normal file
29
bookmarks/templates/bookmarks/user_select.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<form id="user-select" action="" method="get">
|
||||
{% if filters.query %}
|
||||
<input type="hidden" name="q" value="{{ filters.query }}">
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<div class="d-flex">
|
||||
<select name="user" class="form-select">
|
||||
<option value="">Everyone</option>
|
||||
{% for user in users %}
|
||||
<option value="{{ user.username }}"
|
||||
{% if user.username == filters.user %}selected{% endif %}
|
||||
data-is-user-option>
|
||||
{{ user.username }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<noscript>
|
||||
<button type="submit" class="btn btn-link ml-2">Apply</button>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
const form = document.getElementById('user-select');
|
||||
const select = form.querySelector('select');
|
||||
select.addEventListener('change', () => {
|
||||
form.submit();
|
||||
});
|
||||
</script>
|
|
@ -47,6 +47,16 @@
|
|||
case it goes offline or its content is modified.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.enable_sharing.id_for_label }}" class="form-checkbox">
|
||||
{{ form.enable_sharing }}
|
||||
<i class="form-icon"></i> Enable bookmark sharing
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
Allows to share bookmarks with other users, and to view shared bookmarks.
|
||||
Disabling this feature will hide all previously shared bookmarks from other users.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Save" class="btn btn-primary mt-2">
|
||||
</div>
|
||||
|
|
|
@ -3,14 +3,16 @@ from typing import List
|
|||
from django import template
|
||||
from django.core.paginator import Page
|
||||
|
||||
from bookmarks.models import BookmarkForm, Tag, build_tag_string
|
||||
from bookmarks.models import BookmarkForm, BookmarkFilters, Tag, build_tag_string, User
|
||||
from bookmarks.utils import unique
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag('bookmarks/form.html', name='bookmark_form')
|
||||
def bookmark_form(form: BookmarkForm, cancel_url: str, bookmark_id: int = 0, auto_close: bool = False):
|
||||
@register.inclusion_tag('bookmarks/form.html', name='bookmark_form', takes_context=True)
|
||||
def bookmark_form(context, form: BookmarkForm, cancel_url: str, bookmark_id: int = 0, auto_close: bool = False):
|
||||
return {
|
||||
'request': context['request'],
|
||||
'form': form,
|
||||
'auto_close': auto_close,
|
||||
'bookmark_id': bookmark_id,
|
||||
|
@ -25,7 +27,13 @@ class TagGroup:
|
|||
|
||||
|
||||
def create_tag_groups(tags: List[Tag]):
|
||||
sorted_tags = sorted(tags, key=lambda x: str.lower(x.name))
|
||||
# Only display each tag name once, ignoring casing
|
||||
# This covers cases where the tag cloud contains shared tags with duplicate names
|
||||
# Also means that the cloud can not make assumptions that it will necessarily contain
|
||||
# all tags of the current user
|
||||
unique_tags = unique(tags, key=lambda x: str.lower(x.name))
|
||||
# Ensure groups, as well as tags within groups, are ordered alphabetically
|
||||
sorted_tags = sorted(unique_tags, key=lambda x: str.lower(x.name))
|
||||
group = None
|
||||
groups = []
|
||||
|
||||
|
@ -61,11 +69,20 @@ def bookmark_list(context, bookmarks: Page, return_url: str, link_target: str =
|
|||
|
||||
|
||||
@register.inclusion_tag('bookmarks/search.html', name='bookmark_search', takes_context=True)
|
||||
def bookmark_search(context, query: str, tags: [Tag], mode: str = 'default'):
|
||||
def bookmark_search(context, filters: BookmarkFilters, tags: [Tag], mode: str = ''):
|
||||
tag_names = [tag.name for tag in tags]
|
||||
tags_string = build_tag_string(tag_names, ' ')
|
||||
return {
|
||||
'query': query,
|
||||
'filters': filters,
|
||||
'tags_string': tags_string,
|
||||
'mode': mode,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('bookmarks/user_select.html', name='user_select', takes_context=True)
|
||||
def user_select(context, filters: BookmarkFilters, users: List[User]):
|
||||
sorted_users = sorted(users, key=lambda x: str.lower(x.username))
|
||||
return {
|
||||
'filters': filters,
|
||||
'users': sorted_users,
|
||||
}
|
||||
|
|
|
@ -32,6 +32,18 @@ def append_query_param(context, **kwargs):
|
|||
return query.urlencode()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def replace_query_param(context, **kwargs):
|
||||
query = context.request.GET.copy()
|
||||
|
||||
# Create query param or replace existing
|
||||
for key in kwargs:
|
||||
value = kwargs[key]
|
||||
query.__setitem__(key, value)
|
||||
|
||||
return query.urlencode()
|
||||
|
||||
|
||||
@register.filter(name='hash_tag')
|
||||
def hash_tag(tag_name):
|
||||
return '#' + tag_name
|
||||
|
|
|
@ -23,6 +23,7 @@ class BookmarkFactoryMixin:
|
|||
def setup_bookmark(self,
|
||||
is_archived: bool = False,
|
||||
unread: bool = False,
|
||||
shared: bool = False,
|
||||
tags=None,
|
||||
user: User = None,
|
||||
url: str = '',
|
||||
|
@ -52,6 +53,7 @@ class BookmarkFactoryMixin:
|
|||
owner=user,
|
||||
is_archived=is_archived,
|
||||
unread=unread,
|
||||
shared=shared,
|
||||
web_archive_snapshot_url=web_archive_snapshot_url,
|
||||
)
|
||||
bookmark.save()
|
||||
|
@ -69,6 +71,14 @@ class BookmarkFactoryMixin:
|
|||
tag.save()
|
||||
return tag
|
||||
|
||||
def setup_user(self, name: str = None, enable_sharing: bool = False):
|
||||
if not name:
|
||||
name = get_random_string(length=32)
|
||||
user = User.objects.create_user(name, 'user@example.com', 'password123')
|
||||
user.profile.enable_sharing = enable_sharing
|
||||
user.profile.save()
|
||||
return user
|
||||
|
||||
|
||||
class LinkdingApiTestCase(APITestCase):
|
||||
def get(self, url, expected_status_code=status.HTTP_200_OK):
|
||||
|
|
42
bookmarks/tests/test_bookmark_archived_view_performance.py
Normal file
42
bookmarks/tests/test_bookmark_archived_view_performance.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.test import TransactionTestCase
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
from django.db import connections
|
||||
from django.db.utils import DEFAULT_DB_ALIAS
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkArchivedViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def get_connection(self):
|
||||
return connections[DEFAULT_DB_ALIAS]
|
||||
|
||||
def test_should_not_increase_number_of_queries_per_bookmark(self):
|
||||
# create initial bookmarks
|
||||
num_initial_bookmarks = 10
|
||||
for index in range(num_initial_bookmarks):
|
||||
self.setup_bookmark(user=self.user, is_archived=True)
|
||||
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
self.assertContains(response, 'data-is-bookmark-item', num_initial_bookmarks)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
# add more bookmarks
|
||||
num_additional_bookmarks = 10
|
||||
for index in range(num_additional_bookmarks):
|
||||
self.setup_bookmark(user=self.user, is_archived=True)
|
||||
|
||||
# assert num queries doesn't increase
|
||||
with self.assertNumQueries(number_of_queries):
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
self.assertContains(response, 'data-is-bookmark-item', num_initial_bookmarks + num_additional_bookmarks)
|
|
@ -21,6 +21,7 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
'title': 'edited title',
|
||||
'description': 'edited description',
|
||||
'unread': False,
|
||||
'shared': False,
|
||||
}
|
||||
return {**form_data, **overrides}
|
||||
|
||||
|
@ -37,20 +38,37 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
self.assertEqual(bookmark.title, form_data['title'])
|
||||
self.assertEqual(bookmark.description, form_data['description'])
|
||||
self.assertEqual(bookmark.unread, form_data['unread'])
|
||||
self.assertEqual(bookmark.shared, form_data['shared'])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
self.assertEqual(bookmark.tags.all()[0].name, 'editedtag1')
|
||||
self.assertEqual(bookmark.tags.all()[1].name, 'editedtag2')
|
||||
|
||||
def test_should_mark_bookmark_as_unread(self):
|
||||
def test_should_edit_unread_state(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
form_data = self.create_form_data({'id': bookmark.id, 'unread': True})
|
||||
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertTrue(bookmark.unread)
|
||||
|
||||
form_data = self.create_form_data({'id': bookmark.id, 'unread': False})
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
bookmark.refresh_from_db()
|
||||
self.assertFalse(bookmark.unread)
|
||||
|
||||
def test_should_edit_shared_state(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
form_data = self.create_form_data({'id': bookmark.id, 'shared': True})
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
bookmark.refresh_from_db()
|
||||
self.assertTrue(bookmark.shared)
|
||||
|
||||
form_data = self.create_form_data({'id': bookmark.id, 'shared': False})
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
bookmark.refresh_from_db()
|
||||
self.assertFalse(bookmark.shared)
|
||||
|
||||
def test_should_prefill_bookmark_form_fields(self):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
@ -126,3 +144,32 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
self.assertNotEqual(bookmark.url, form_data['url'])
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_should_respect_share_profile_setting(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
self.user.profile.enable_sharing = False
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
<label for="id_shared" class="form-checkbox">
|
||||
<input type="checkbox" name="shared" id="id_shared">
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
''', html, count=0)
|
||||
|
||||
self.user.profile.enable_sharing = True
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
<label for="id_shared" class="form-checkbox">
|
||||
<input type="checkbox" name="shared" id="id_shared">
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
''', html, count=1)
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import urllib.parse
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
@ -156,3 +158,30 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
response = self.client.get(reverse('bookmarks:index'))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
|
||||
|
||||
def test_edit_link_return_url_should_contain_query_params(self):
|
||||
bookmark = self.setup_bookmark(title='foo')
|
||||
|
||||
# without query params
|
||||
url = reverse('bookmarks:index')
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||
return_url = urllib.parse.quote_plus(url)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<a href="{edit_url}?return_url={return_url}"
|
||||
class="btn btn-link btn-sm">Edit</a>
|
||||
''', html)
|
||||
|
||||
# with query params
|
||||
url = reverse('bookmarks:index') + '?q=foo&user=user'
|
||||
response = self.client.get(url)
|
||||
html = response.content.decode()
|
||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||
return_url = urllib.parse.quote_plus(url)
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<a href="{edit_url}?return_url={return_url}"
|
||||
class="btn btn-link btn-sm">Edit</a>
|
||||
''', html)
|
||||
|
|
42
bookmarks/tests/test_bookmark_index_view_performance.py
Normal file
42
bookmarks/tests/test_bookmark_index_view_performance.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.test import TransactionTestCase
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
from django.db import connections
|
||||
from django.db.utils import DEFAULT_DB_ALIAS
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkIndexViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def get_connection(self):
|
||||
return connections[DEFAULT_DB_ALIAS]
|
||||
|
||||
def test_should_not_increase_number_of_queries_per_bookmark(self):
|
||||
# create initial bookmarks
|
||||
num_initial_bookmarks = 10
|
||||
for index in range(num_initial_bookmarks):
|
||||
self.setup_bookmark(user=self.user)
|
||||
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
self.assertContains(response, 'data-is-bookmark-item', num_initial_bookmarks)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
# add more bookmarks
|
||||
num_additional_bookmarks = 10
|
||||
for index in range(num_additional_bookmarks):
|
||||
self.setup_bookmark(user=self.user)
|
||||
|
||||
# assert num queries doesn't increase
|
||||
with self.assertNumQueries(number_of_queries):
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
self.assertContains(response, 'data-is-bookmark-item', num_initial_bookmarks + num_additional_bookmarks)
|
|
@ -20,6 +20,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
'title': 'test title',
|
||||
'description': 'test description',
|
||||
'unread': False,
|
||||
'shared': False,
|
||||
'auto_close': '',
|
||||
}
|
||||
return {**form_data, **overrides}
|
||||
|
@ -37,6 +38,7 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
self.assertEqual(bookmark.title, form_data['title'])
|
||||
self.assertEqual(bookmark.description, form_data['description'])
|
||||
self.assertEqual(bookmark.unread, form_data['unread'])
|
||||
self.assertEqual(bookmark.shared, form_data['shared'])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
self.assertEqual(bookmark.tags.all()[0].name, 'tag1')
|
||||
self.assertEqual(bookmark.tags.all()[1].name, 'tag2')
|
||||
|
@ -51,6 +53,16 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
bookmark = Bookmark.objects.first()
|
||||
self.assertTrue(bookmark.unread)
|
||||
|
||||
def test_should_create_new_shared_bookmark(self):
|
||||
form_data = self.create_form_data({'shared': True})
|
||||
|
||||
self.client.post(reverse('bookmarks:new'), form_data)
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 1)
|
||||
|
||||
bookmark = Bookmark.objects.first()
|
||||
self.assertTrue(bookmark.shared)
|
||||
|
||||
def test_should_prefill_url_from_url_parameter(self):
|
||||
response = self.client.get(reverse('bookmarks:new') + '?url=http://example.com')
|
||||
html = response.content.decode()
|
||||
|
@ -98,3 +110,30 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
response = self.client.post(reverse('bookmarks:new'), form_data)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
||||
|
||||
def test_should_respect_share_profile_setting(self):
|
||||
self.user.profile.enable_sharing = False
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:new'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
<label for="id_shared" class="form-checkbox">
|
||||
<input type="checkbox" name="shared" id="id_shared">
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
''', html, count=0)
|
||||
|
||||
self.user.profile.enable_sharing = True
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:new'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('''
|
||||
<label for="id_shared" class="form-checkbox">
|
||||
<input type="checkbox" name="shared" id="id_shared">
|
||||
<i class="form-icon"></i>
|
||||
<span>Share</span>
|
||||
</label>
|
||||
''', html, count=1)
|
||||
|
|
40
bookmarks/tests/test_bookmark_search_tag.py
Normal file
40
bookmarks/tests/test_bookmark_search_tag.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
from django.db.models import QuerySet
|
||||
from django.template import Template, RequestContext
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
from bookmarks.models import BookmarkFilters, Tag
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin):
|
||||
def render_template(self, url: str, tags: QuerySet[Tag] = Tag.objects.all()):
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
filters = BookmarkFilters(request)
|
||||
context = RequestContext(request, {
|
||||
'request': request,
|
||||
'filters': filters,
|
||||
'tags': tags,
|
||||
})
|
||||
template_to_render = Template(
|
||||
'{% load bookmarks %}'
|
||||
'{% bookmark_search filters tags %}'
|
||||
)
|
||||
return template_to_render.render(context)
|
||||
|
||||
def test_render_hidden_inputs_for_filter_params(self):
|
||||
# Should render hidden inputs if query param exists
|
||||
url = '/test?q=foo&user=john'
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertInHTML('''
|
||||
<input type="hidden" name="user" value="john">
|
||||
''', rendered_template)
|
||||
|
||||
# Should not render hidden inputs if query param does not exist
|
||||
url = '/test?q=foo'
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertInHTML('''
|
||||
<input type="hidden" name="user" value="john">
|
||||
''', rendered_template, count=0)
|
255
bookmarks/tests/test_bookmark_shared_view.py
Normal file
255
bookmarks/tests/test_bookmark_shared_view.py
Normal file
|
@ -0,0 +1,255 @@
|
|||
from typing import List
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import Bookmark, Tag, UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkSharedViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertBookmarkCount(self, html: str, bookmark: Bookmark, count: int, link_target: str = '_blank'):
|
||||
self.assertInHTML(
|
||||
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener" class="">{bookmark.resolved_title}</a>',
|
||||
html, count=count
|
||||
)
|
||||
|
||||
def assertVisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
|
||||
html = response.content.decode()
|
||||
self.assertContains(response, 'data-is-bookmark-item', count=len(bookmarks))
|
||||
|
||||
for bookmark in bookmarks:
|
||||
self.assertBookmarkCount(html, bookmark, 1, link_target)
|
||||
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: List[Bookmark], link_target: str = '_blank'):
|
||||
html = response.content.decode()
|
||||
|
||||
for bookmark in bookmarks:
|
||||
self.assertBookmarkCount(html, bookmark, 0, link_target)
|
||||
|
||||
def assertVisibleTags(self, response, tags: [Tag]):
|
||||
self.assertContains(response, 'data-is-tag-item', count=len(tags))
|
||||
|
||||
for tag in tags:
|
||||
self.assertContains(response, tag.name)
|
||||
|
||||
def assertInvisibleTags(self, response, tags: [Tag]):
|
||||
for tag in tags:
|
||||
self.assertNotContains(response, tag.name)
|
||||
|
||||
def assertVisibleUserOptions(self, response, users: List[User]):
|
||||
html = response.content.decode()
|
||||
self.assertContains(response, 'data-is-user-option', count=len(users))
|
||||
|
||||
for user in users:
|
||||
self.assertInHTML(f'''
|
||||
<option value="{user.username}" data-is-user-option>
|
||||
{user.username}
|
||||
</option>
|
||||
''', html)
|
||||
|
||||
def assertInvisibleUserOptions(self, response, users: List[User]):
|
||||
html = response.content.decode()
|
||||
|
||||
for user in users:
|
||||
self.assertInHTML(f'''
|
||||
<option value="{user.username}" data-is-user-option>
|
||||
{user.username}
|
||||
</option>
|
||||
''', html, count=0)
|
||||
|
||||
def test_should_list_shared_bookmarks_from_all_users_that_have_sharing_enabled(self):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
user4 = self.setup_user(enable_sharing=False)
|
||||
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(shared=True, user=user1),
|
||||
self.setup_bookmark(shared=True, user=user2),
|
||||
self.setup_bookmark(shared=True, user=user3),
|
||||
]
|
||||
invisible_bookmarks = [
|
||||
self.setup_bookmark(shared=False, user=user1),
|
||||
self.setup_bookmark(shared=False, user=user2),
|
||||
self.setup_bookmark(shared=False, user=user3),
|
||||
self.setup_bookmark(shared=True, user=user4),
|
||||
]
|
||||
|
||||
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_shared_bookmarks_from_selected_user(self):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(shared=True, user=user1),
|
||||
]
|
||||
invisible_bookmarks = [
|
||||
self.setup_bookmark(shared=True, user=user2),
|
||||
self.setup_bookmark(shared=True, user=user3),
|
||||
]
|
||||
|
||||
url = reverse('bookmarks:shared') + '?user=' + user1.username
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_bookmarks_matching_query(self):
|
||||
user = self.setup_user(enable_sharing=True)
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(shared=True, title='searchvalue', user=user),
|
||||
self.setup_bookmark(shared=True, title='searchvalue', user=user),
|
||||
self.setup_bookmark(shared=True, title='searchvalue', user=user)
|
||||
]
|
||||
invisible_bookmarks = [
|
||||
self.setup_bookmark(shared=True, user=user),
|
||||
self.setup_bookmark(shared=True, user=user),
|
||||
self.setup_bookmark(shared=True, user=user)
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared') + '?q=searchvalue')
|
||||
|
||||
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):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
user4 = self.setup_user(enable_sharing=False)
|
||||
visible_tags = [
|
||||
self.setup_tag(user=user1),
|
||||
self.setup_tag(user=user2),
|
||||
self.setup_tag(user=user3),
|
||||
]
|
||||
invisible_tags = [
|
||||
self.setup_tag(user=user1),
|
||||
self.setup_tag(user=user2),
|
||||
self.setup_tag(user=user3),
|
||||
self.setup_tag(user=user4),
|
||||
]
|
||||
|
||||
self.setup_bookmark(shared=True, user=user1, tags=[visible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user2, tags=[visible_tags[1]])
|
||||
self.setup_bookmark(shared=True, user=user3, tags=[visible_tags[2]])
|
||||
|
||||
self.setup_bookmark(shared=False, user=user1, tags=[invisible_tags[0]])
|
||||
self.setup_bookmark(shared=False, user=user2, tags=[invisible_tags[1]])
|
||||
self.setup_bookmark(shared=False, user=user3, tags=[invisible_tags[2]])
|
||||
self.setup_bookmark(shared=True, user=user4, tags=[invisible_tags[3]])
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_list_tags_for_shared_bookmarks_from_selected_user(self):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
visible_tags = [
|
||||
self.setup_tag(user=user1),
|
||||
]
|
||||
invisible_tags = [
|
||||
self.setup_tag(user=user2),
|
||||
self.setup_tag(user=user3),
|
||||
]
|
||||
|
||||
self.setup_bookmark(shared=True, user=user1, tags=[visible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[1]])
|
||||
|
||||
url = reverse('bookmarks:shared') + '?user=' + user1.username
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_list_tags_for_bookmarks_matching_query(self):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
visible_tags = [
|
||||
self.setup_tag(user=user1),
|
||||
self.setup_tag(user=user2),
|
||||
self.setup_tag(user=user3),
|
||||
]
|
||||
invisible_tags = [
|
||||
self.setup_tag(user=user1),
|
||||
self.setup_tag(user=user2),
|
||||
self.setup_tag(user=user3),
|
||||
]
|
||||
|
||||
self.setup_bookmark(shared=True, user=user1, title='searchvalue', tags=[visible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user2, title='searchvalue', tags=[visible_tags[1]])
|
||||
self.setup_bookmark(shared=True, user=user3, title='searchvalue', tags=[visible_tags[2]])
|
||||
|
||||
self.setup_bookmark(shared=True, user=user1, tags=[invisible_tags[0]])
|
||||
self.setup_bookmark(shared=True, user=user2, tags=[invisible_tags[1]])
|
||||
self.setup_bookmark(shared=True, user=user3, tags=[invisible_tags[2]])
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared') + '?q=searchvalue')
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_list_users_with_shared_bookmarks_if_sharing_is_enabled(self):
|
||||
expected_visible_users = [
|
||||
self.setup_user(enable_sharing=True),
|
||||
self.setup_user(enable_sharing=True),
|
||||
]
|
||||
self.setup_bookmark(shared=True, user=expected_visible_users[0])
|
||||
self.setup_bookmark(shared=True, user=expected_visible_users[1])
|
||||
|
||||
expected_invisible_users = [
|
||||
self.setup_user(enable_sharing=True),
|
||||
self.setup_user(enable_sharing=False),
|
||||
]
|
||||
self.setup_bookmark(shared=False, user=expected_invisible_users[0])
|
||||
self.setup_bookmark(shared=True, user=expected_invisible_users[1])
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
self.assertVisibleUserOptions(response, expected_visible_users)
|
||||
self.assertInvisibleUserOptions(response, expected_invisible_users)
|
||||
|
||||
|
||||
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(shared=True),
|
||||
self.setup_bookmark(shared=True),
|
||||
self.setup_bookmark(shared=True)
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_blank')
|
||||
|
||||
|
||||
def test_should_open_bookmarks_in_same_page_if_specified_in_user_profile(self):
|
||||
user = self.get_or_create_test_user()
|
||||
user.profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
|
||||
user.profile.save()
|
||||
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(shared=True),
|
||||
self.setup_bookmark(shared=True),
|
||||
self.setup_bookmark(shared=True)
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
|
44
bookmarks/tests/test_bookmark_shared_view_performance.py
Normal file
44
bookmarks/tests/test_bookmark_shared_view_performance.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.test import TransactionTestCase
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
from django.db import connections
|
||||
from django.db.utils import DEFAULT_DB_ALIAS
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkSharedViewPerformanceTestCase(TransactionTestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def get_connection(self):
|
||||
return connections[DEFAULT_DB_ALIAS]
|
||||
|
||||
def test_should_not_increase_number_of_queries_per_bookmark(self):
|
||||
# create initial users and bookmarks
|
||||
num_initial_bookmarks = 10
|
||||
for index in range(num_initial_bookmarks):
|
||||
user = self.setup_user(enable_sharing=True)
|
||||
self.setup_bookmark(user=user, shared=True)
|
||||
|
||||
# capture number of queries
|
||||
context = CaptureQueriesContext(self.get_connection())
|
||||
with context:
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
self.assertContains(response, 'data-is-bookmark-item', num_initial_bookmarks)
|
||||
|
||||
number_of_queries = context.final_queries
|
||||
|
||||
# add more users and bookmarks
|
||||
num_additional_bookmarks = 10
|
||||
for index in range(num_additional_bookmarks):
|
||||
user = self.setup_user(enable_sharing=True)
|
||||
self.setup_bookmark(user=user, shared=True)
|
||||
|
||||
# assert num queries doesn't increase
|
||||
with self.assertNumQueries(number_of_queries):
|
||||
response = self.client.get(reverse('bookmarks:shared'))
|
||||
self.assertContains(response, 'data-is-bookmark-item', num_initial_bookmarks + num_additional_bookmarks)
|
|
@ -36,6 +36,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
expectation['website_description'] = bookmark.website_description
|
||||
expectation['is_archived'] = bookmark.is_archived
|
||||
expectation['unread'] = bookmark.unread
|
||||
expectation['shared'] = bookmark.shared
|
||||
expectation['tag_names'] = tag_names
|
||||
expectation['date_added'] = bookmark.date_added.isoformat().replace('+00:00', 'Z')
|
||||
expectation['date_modified'] = bookmark.date_modified.isoformat().replace('+00:00', 'Z')
|
||||
|
@ -64,6 +65,66 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
expected_status_code=status.HTTP_200_OK)
|
||||
self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1])
|
||||
|
||||
def test_list_shared_bookmarks(self):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
user4 = self.setup_user(enable_sharing=False)
|
||||
shared_bookmarks = [
|
||||
self.setup_bookmark(shared=True, user=user1),
|
||||
self.setup_bookmark(shared=True, user=user2),
|
||||
self.setup_bookmark(shared=True, user=user3),
|
||||
]
|
||||
# Unshared bookmarks
|
||||
self.setup_bookmark(shared=False, user=user1)
|
||||
self.setup_bookmark(shared=False, user=user2)
|
||||
self.setup_bookmark(shared=False, user=user3)
|
||||
self.setup_bookmark(shared=True, user=user4)
|
||||
|
||||
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):
|
||||
# Search by query
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
expected_bookmarks = [
|
||||
self.setup_bookmark(title='searchvalue', shared=True, user=user1),
|
||||
self.setup_bookmark(title='searchvalue', shared=True, user=user2),
|
||||
self.setup_bookmark(title='searchvalue', shared=True, user=user3),
|
||||
]
|
||||
self.setup_bookmark(shared=True, user=user1),
|
||||
self.setup_bookmark(shared=True, user=user2),
|
||||
self.setup_bookmark(shared=True, user=user3),
|
||||
|
||||
response = self.get(reverse('bookmarks:bookmark-shared') + '?q=searchvalue',
|
||||
expected_status_code=status.HTTP_200_OK)
|
||||
self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)
|
||||
|
||||
# Search by user
|
||||
user_search_user = self.setup_user(enable_sharing=True)
|
||||
expected_bookmarks = [
|
||||
self.setup_bookmark(shared=True, user=user_search_user),
|
||||
self.setup_bookmark(shared=True, user=user_search_user),
|
||||
self.setup_bookmark(shared=True, user=user_search_user),
|
||||
]
|
||||
response = self.get(reverse('bookmarks:bookmark-shared') + '?user=' + user_search_user.username,
|
||||
expected_status_code=status.HTTP_200_OK)
|
||||
self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)
|
||||
|
||||
# Search by query and user
|
||||
combined_search_user = self.setup_user(enable_sharing=True)
|
||||
expected_bookmarks = [
|
||||
self.setup_bookmark(title='searchvalue', shared=True, user=combined_search_user),
|
||||
self.setup_bookmark(title='searchvalue', shared=True, user=combined_search_user),
|
||||
self.setup_bookmark(title='searchvalue', shared=True, user=combined_search_user),
|
||||
]
|
||||
response = self.get(
|
||||
reverse('bookmarks:bookmark-shared') + '?q=searchvalue&user=' + combined_search_user.username,
|
||||
expected_status_code=status.HTTP_200_OK)
|
||||
self.assertBookmarkListEqual(response.data['results'], expected_bookmarks)
|
||||
|
||||
def test_create_bookmark(self):
|
||||
data = {
|
||||
'url': 'https://example.com/',
|
||||
|
@ -71,6 +132,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
'description': 'Test description',
|
||||
'is_archived': False,
|
||||
'unread': False,
|
||||
'shared': False,
|
||||
'tag_names': ['tag1', 'tag2']
|
||||
}
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
|
@ -80,6 +142,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
self.assertEqual(bookmark.description, data['description'])
|
||||
self.assertFalse(bookmark.is_archived, data['is_archived'])
|
||||
self.assertFalse(bookmark.unread, data['unread'])
|
||||
self.assertFalse(bookmark.shared, data['shared'])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
|
||||
|
@ -91,6 +154,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
'title': 'Updated title',
|
||||
'description': 'Updated description',
|
||||
'unread': True,
|
||||
'shared': True,
|
||||
'is_archived': True,
|
||||
'tag_names': ['tag1', 'tag2']
|
||||
}
|
||||
|
@ -103,6 +167,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
# Saving a duplicate bookmark should not modify archive flag - right?
|
||||
self.assertFalse(bookmark.is_archived)
|
||||
self.assertEqual(bookmark.unread, data['unread'])
|
||||
self.assertEqual(bookmark.shared, data['shared'])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
|
||||
|
@ -159,6 +224,18 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
bookmark = Bookmark.objects.get(url=data['url'])
|
||||
self.assertFalse(bookmark.unread)
|
||||
|
||||
def test_create_shared_bookmark(self):
|
||||
data = {'url': 'https://example.com/', 'shared': True}
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
bookmark = Bookmark.objects.get(url=data['url'])
|
||||
self.assertTrue(bookmark.shared)
|
||||
|
||||
def test_create_bookmark_is_not_shared_by_default(self):
|
||||
data = {'url': 'https://example.com/'}
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
bookmark = Bookmark.objects.get(url=data['url'])
|
||||
self.assertFalse(bookmark.shared)
|
||||
|
||||
def test_get_bookmark(self):
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
response = self.get(url, expected_status_code=status.HTTP_200_OK)
|
||||
|
@ -193,6 +270,13 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id)
|
||||
self.assertEqual(updated_bookmark.unread, True)
|
||||
|
||||
def test_update_bookmark_shared_flag(self):
|
||||
data = {'url': 'https://example.com/', 'shared': True}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id)
|
||||
self.assertEqual(updated_bookmark.shared, True)
|
||||
|
||||
def test_patch_bookmark(self):
|
||||
data = {'url': 'https://example.com'}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
|
@ -224,6 +308,18 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
self.bookmark1.refresh_from_db()
|
||||
self.assertFalse(self.bookmark1.unread)
|
||||
|
||||
data = {'shared': True}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
self.bookmark1.refresh_from_db()
|
||||
self.assertTrue(self.bookmark1.shared)
|
||||
|
||||
data = {'shared': False}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
self.bookmark1.refresh_from_db()
|
||||
self.assertFalse(self.bookmark1.shared)
|
||||
|
||||
data = {'tag_names': ['updated-tag-1', 'updated-tag-2']}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
|
@ -260,6 +356,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
def test_can_only_access_own_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
inaccessible_bookmark = self.setup_bookmark(user=other_user)
|
||||
inaccessible_shared_bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||
self.setup_bookmark(user=other_user, is_archived=True)
|
||||
|
||||
url = reverse('bookmarks:bookmark-list')
|
||||
|
@ -273,14 +370,29 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
|||
url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id])
|
||||
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-detail', args=[inaccessible_shared_bookmark.id])
|
||||
self.get(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id])
|
||||
self.put(url, {url: 'https://example.com/'}, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-detail', args=[inaccessible_shared_bookmark.id])
|
||||
self.put(url, {url: 'https://example.com/'}, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-detail', args=[inaccessible_bookmark.id])
|
||||
self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-detail', args=[inaccessible_shared_bookmark.id])
|
||||
self.delete(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-archive', args=[inaccessible_bookmark.id])
|
||||
self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-archive', args=[inaccessible_shared_bookmark.id])
|
||||
self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-unarchive', args=[inaccessible_bookmark.id])
|
||||
self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = reverse('bookmarks:bookmark-unarchive', args=[inaccessible_shared_bookmark.id])
|
||||
self.post(url, expected_status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
|
|
@ -2,9 +2,10 @@ from dateutil.relativedelta import relativedelta
|
|||
from django.core.paginator import Paginator
|
||||
from django.template import Template, RequestContext
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone, formats
|
||||
|
||||
from bookmarks.models import Bookmark, UserProfile
|
||||
from bookmarks.models import Bookmark, UserProfile, User
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
|
@ -41,9 +42,46 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||
<span class="text-gray text-sm">|</span>
|
||||
''', html)
|
||||
|
||||
def render_template(self, bookmarks: [Bookmark], template: Template) -> str:
|
||||
def assertBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkActionsCount(html, bookmark, count=1)
|
||||
|
||||
def assertNoBookmarkActions(self, html: str, bookmark: Bookmark):
|
||||
self.assertBookmarkActionsCount(html, bookmark, count=0)
|
||||
|
||||
def assertBookmarkActionsCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
# Edit link
|
||||
edit_url = reverse('bookmarks:edit', args=[bookmark.id])
|
||||
self.assertInHTML(f'''
|
||||
<a href="{edit_url}?return_url=/test"
|
||||
class="btn btn-link btn-sm">Edit</a>
|
||||
''', html, count=count)
|
||||
# Archive link
|
||||
self.assertInHTML(f'''
|
||||
<button type="submit" name="archive" value="{bookmark.id}"
|
||||
class="btn btn-link btn-sm">Archive</button>
|
||||
''', html, count=count)
|
||||
# Delete link
|
||||
self.assertInHTML(f'''
|
||||
<button type="submit" name="remove" value="{bookmark.id}"
|
||||
class="btn btn-link btn-sm btn-confirmation">Remove</button>
|
||||
''', html, count=count)
|
||||
|
||||
def assertShareInfo(self, html: str, bookmark: Bookmark):
|
||||
self.assertShareInfoCount(html, bookmark, 1)
|
||||
|
||||
def assertNoShareInfo(self, html: str, bookmark: Bookmark):
|
||||
self.assertShareInfoCount(html, bookmark, 0)
|
||||
|
||||
def assertShareInfoCount(self, html: str, bookmark: Bookmark, count=1):
|
||||
self.assertInHTML(f'''
|
||||
<span class="text-gray text-sm">Shared by
|
||||
<a class="text-gray" href="?user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||
</span>
|
||||
''', html, count=count)
|
||||
|
||||
def render_template(self, bookmarks: [Bookmark], template: Template, url: str = '/test') -> str:
|
||||
rf = RequestFactory()
|
||||
request = rf.get('/test')
|
||||
request = rf.get(url)
|
||||
request.user = self.get_or_create_test_user()
|
||||
paginator = Paginator(bookmarks, 10)
|
||||
page = paginator.page(1)
|
||||
|
@ -51,12 +89,12 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||
context = RequestContext(request, {'bookmarks': page, 'return_url': '/test'})
|
||||
return template.render(context)
|
||||
|
||||
def render_default_template(self, bookmarks: [Bookmark]) -> str:
|
||||
def render_default_template(self, bookmarks: [Bookmark], url: str = '/test') -> str:
|
||||
template = Template(
|
||||
'{% load bookmarks %}'
|
||||
'{% bookmark_list bookmarks return_url %}'
|
||||
)
|
||||
return self.render_template(bookmarks, template)
|
||||
return self.render_template(bookmarks, template, url)
|
||||
|
||||
def render_template_with_link_target(self, bookmarks: [Bookmark], link_target: str) -> str:
|
||||
template = Template(
|
||||
|
@ -147,3 +185,29 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
|||
html = self.render_template_with_link_target([bookmark], '_self')
|
||||
|
||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_self')
|
||||
|
||||
def test_show_bookmark_actions_for_owned_bookmarks(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertBookmarkActions(html, bookmark)
|
||||
self.assertNoShareInfo(html, bookmark)
|
||||
|
||||
def test_show_share_info_for_non_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertNoBookmarkActions(html, bookmark)
|
||||
self.assertShareInfo(html, bookmark)
|
||||
|
||||
def test_share_info_user_link_keeps_query_params(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
bookmark = self.setup_bookmark(user=other_user)
|
||||
html = self.render_default_template([bookmark], url='/test?q=foo')
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<span class="text-gray text-sm">Shared by
|
||||
<a class="text-gray" href="?q=foo&user={bookmark.owner.username}">{bookmark.owner.username}</a>
|
||||
</span>
|
||||
''', html)
|
||||
|
|
|
@ -19,11 +19,12 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||
self.get_or_create_test_user()
|
||||
|
||||
def test_create_should_update_existing_bookmark_with_same_url(self):
|
||||
original_bookmark = self.setup_bookmark(url='https://example.com', unread=False)
|
||||
original_bookmark = self.setup_bookmark(url='https://example.com', unread=False, shared=False)
|
||||
bookmark_data = Bookmark(url='https://example.com',
|
||||
title='Updated Title',
|
||||
description='Updated description',
|
||||
unread=True,
|
||||
shared=True,
|
||||
is_archived=True)
|
||||
updated_bookmark = create_bookmark(bookmark_data, '', self.get_or_create_test_user())
|
||||
|
||||
|
@ -32,6 +33,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||
self.assertEqual(updated_bookmark.title, bookmark_data.title)
|
||||
self.assertEqual(updated_bookmark.description, bookmark_data.description)
|
||||
self.assertEqual(updated_bookmark.unread, bookmark_data.unread)
|
||||
self.assertEqual(updated_bookmark.shared, bookmark_data.shared)
|
||||
# Saving a duplicate bookmark should not modify archive flag - right?
|
||||
self.assertFalse(updated_bookmark.is_archived)
|
||||
|
||||
|
|
30
bookmarks/tests/test_nav_menu.py
Normal file
30
bookmarks/tests/test_nav_menu.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class NavMenuTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def test_should_respect_share_profile_setting(self):
|
||||
self.user.profile.enable_sharing = False
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<a href="{reverse('bookmarks:shared')}" class="btn btn-link">Shared</a>
|
||||
''', html, count=0)
|
||||
|
||||
self.user.profile.enable_sharing = True
|
||||
self.user.profile.save()
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<a href="{reverse('bookmarks:shared')}" class="btn btn-link">Shared</a>
|
||||
''', html, count=2)
|
|
@ -572,3 +572,86 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
|||
|
||||
query = queries.query_archived_bookmark_tags(self.user, f'!untagged #{tag.name}')
|
||||
self.assertCountEqual(list(query), [])
|
||||
|
||||
def test_query_shared_bookmarks(self):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
user4 = self.setup_user(enable_sharing=False)
|
||||
tag = self.setup_tag()
|
||||
|
||||
shared_bookmarks = [
|
||||
self.setup_bookmark(user=user1, shared=True, title='test title'),
|
||||
self.setup_bookmark(user=user2, shared=True),
|
||||
self.setup_bookmark(user=user3, shared=True, tags=[tag]),
|
||||
]
|
||||
|
||||
# Unshared bookmarks
|
||||
self.setup_bookmark(user=user1, shared=False, title='test title'),
|
||||
self.setup_bookmark(user=user2, shared=False),
|
||||
self.setup_bookmark(user=user3, shared=False, tags=[tag]),
|
||||
self.setup_bookmark(user=user4, shared=True, tags=[tag]),
|
||||
|
||||
# Should return shared bookmarks from all users
|
||||
query_set = queries.query_shared_bookmarks(None, '')
|
||||
self.assertQueryResult(query_set, [shared_bookmarks])
|
||||
|
||||
# Should respect search query
|
||||
query_set = queries.query_shared_bookmarks(None, 'test title')
|
||||
self.assertQueryResult(query_set, [[shared_bookmarks[0]]])
|
||||
|
||||
query_set = queries.query_shared_bookmarks(None, '#' + tag.name)
|
||||
self.assertQueryResult(query_set, [[shared_bookmarks[2]]])
|
||||
|
||||
def test_query_shared_bookmark_tags(self):
|
||||
user1 = self.setup_user(enable_sharing=True)
|
||||
user2 = self.setup_user(enable_sharing=True)
|
||||
user3 = self.setup_user(enable_sharing=True)
|
||||
user4 = self.setup_user(enable_sharing=False)
|
||||
|
||||
shared_tags = [
|
||||
self.setup_tag(user=user1),
|
||||
self.setup_tag(user=user2),
|
||||
self.setup_tag(user=user3),
|
||||
]
|
||||
|
||||
self.setup_bookmark(user=user1, shared=True, tags=[shared_tags[0]]),
|
||||
self.setup_bookmark(user=user2, shared=True, tags=[shared_tags[1]]),
|
||||
self.setup_bookmark(user=user3, shared=True, tags=[shared_tags[2]]),
|
||||
|
||||
self.setup_bookmark(user=user1, shared=False, tags=[self.setup_tag(user=user1)]),
|
||||
self.setup_bookmark(user=user2, shared=False, tags=[self.setup_tag(user=user2)]),
|
||||
self.setup_bookmark(user=user3, shared=False, tags=[self.setup_tag(user=user3)]),
|
||||
self.setup_bookmark(user=user4, shared=True, tags=[self.setup_tag(user=user4)]),
|
||||
|
||||
query_set = queries.query_shared_bookmark_tags(None, '')
|
||||
|
||||
self.assertQueryResult(query_set, [shared_tags])
|
||||
|
||||
def test_query_shared_bookmark_users(self):
|
||||
users_with_shared_bookmarks = [
|
||||
self.setup_user(enable_sharing=True),
|
||||
self.setup_user(enable_sharing=True),
|
||||
]
|
||||
users_without_shared_bookmarks = [
|
||||
self.setup_user(enable_sharing=True),
|
||||
self.setup_user(enable_sharing=True),
|
||||
self.setup_user(enable_sharing=False),
|
||||
]
|
||||
|
||||
# Shared bookmarks
|
||||
self.setup_bookmark(user=users_with_shared_bookmarks[0], shared=True, title='test title'),
|
||||
self.setup_bookmark(user=users_with_shared_bookmarks[1], shared=True),
|
||||
|
||||
# Unshared bookmarks
|
||||
self.setup_bookmark(user=users_without_shared_bookmarks[0], shared=False, title='test title'),
|
||||
self.setup_bookmark(user=users_without_shared_bookmarks[1], shared=False),
|
||||
self.setup_bookmark(user=users_without_shared_bookmarks[2], shared=True),
|
||||
|
||||
# Should return users with shared bookmarks
|
||||
query_set = queries.query_shared_bookmark_users('')
|
||||
self.assertQueryResult(query_set, [users_with_shared_bookmarks])
|
||||
|
||||
# Should respect search query
|
||||
query_set = queries.query_shared_bookmark_users('test title')
|
||||
self.assertQueryResult(query_set, [[users_with_shared_bookmarks[0]]])
|
||||
|
|
|
@ -34,6 +34,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
'bookmark_date_display': UserProfile.BOOKMARK_DATE_DISPLAY_HIDDEN,
|
||||
'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_SELF,
|
||||
'web_archive_integration': UserProfile.WEB_ARCHIVE_INTEGRATION_ENABLED,
|
||||
'enable_sharing': True,
|
||||
}
|
||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
|
||||
|
@ -44,6 +45,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||
self.assertEqual(self.user.profile.bookmark_date_display, form_data['bookmark_date_display'])
|
||||
self.assertEqual(self.user.profile.bookmark_link_target, form_data['bookmark_link_target'])
|
||||
self.assertEqual(self.user.profile.web_archive_integration, form_data['web_archive_integration'])
|
||||
self.assertEqual(self.user.profile.enable_sharing, form_data['enable_sharing'])
|
||||
|
||||
def test_about_shows_version_info(self):
|
||||
response = self.client.get(reverse('bookmarks:settings.general'))
|
||||
|
|
94
bookmarks/tests/test_tag_cloud_tag.py
Normal file
94
bookmarks/tests/test_tag_cloud_tag.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
from typing import List
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from django.template import Template, RequestContext
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
from bookmarks.models import Tag, User
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class TagCloudTagTest(TestCase, BookmarkFactoryMixin):
|
||||
def make_soup(self, html: str):
|
||||
return BeautifulSoup(html, features="html.parser")
|
||||
|
||||
def render_template(self, tags: List[Tag], url: str = '/test'):
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
context = RequestContext(request, {
|
||||
'request': request,
|
||||
'tags': tags,
|
||||
})
|
||||
template_to_render = Template(
|
||||
'{% load bookmarks %}'
|
||||
'{% tag_cloud tags %}'
|
||||
)
|
||||
return template_to_render.render(context)
|
||||
|
||||
def assertTagGroups(self, rendered_template: str, groups: List[List[str]]):
|
||||
soup = self.make_soup(rendered_template)
|
||||
group_elements = soup.select('p.group')
|
||||
|
||||
self.assertEqual(len(group_elements), len(groups))
|
||||
|
||||
for group_index, tags in enumerate(groups, start=0):
|
||||
group_element = group_elements[group_index]
|
||||
link_elements = group_element.select('a')
|
||||
|
||||
self.assertEqual(len(link_elements), len(tags))
|
||||
|
||||
for tag_index, tag in enumerate(tags, start=0):
|
||||
link_element = link_elements[tag_index]
|
||||
self.assertEqual(link_element.text.strip(), tag)
|
||||
|
||||
def test_group_alphabetically(self):
|
||||
tags = [
|
||||
self.setup_tag(name='Cockatoo'),
|
||||
self.setup_tag(name='Badger'),
|
||||
self.setup_tag(name='Buffalo'),
|
||||
self.setup_tag(name='Chihuahua'),
|
||||
self.setup_tag(name='Alpaca'),
|
||||
self.setup_tag(name='Coyote'),
|
||||
self.setup_tag(name='Aardvark'),
|
||||
self.setup_tag(name='Bumblebee'),
|
||||
self.setup_tag(name='Armadillo'),
|
||||
]
|
||||
|
||||
rendered_template = self.render_template(tags)
|
||||
|
||||
self.assertTagGroups(rendered_template, [
|
||||
[
|
||||
'Aardvark',
|
||||
'Alpaca',
|
||||
'Armadillo',
|
||||
],
|
||||
[
|
||||
'Badger',
|
||||
'Buffalo',
|
||||
'Bumblebee',
|
||||
],
|
||||
[
|
||||
'Chihuahua',
|
||||
'Cockatoo',
|
||||
'Coyote',
|
||||
],
|
||||
])
|
||||
|
||||
def test_no_duplicate_tag_names(self):
|
||||
user1 = User.objects.create_user('user1', 'user1@example.com', 'password123')
|
||||
user2 = User.objects.create_user('user2', 'user2@example.com', 'password123')
|
||||
user3 = User.objects.create_user('user3', 'user3@example.com', 'password123')
|
||||
|
||||
tags = [
|
||||
self.setup_tag(name='shared', user=user1),
|
||||
self.setup_tag(name='shared', user=user2),
|
||||
self.setup_tag(name='shared', user=user3),
|
||||
]
|
||||
|
||||
rendered_template = self.render_template(tags)
|
||||
|
||||
self.assertTagGroups(rendered_template, [
|
||||
[
|
||||
'shared',
|
||||
],
|
||||
])
|
|
@ -10,3 +10,8 @@ class UserProfileTestCase(TestCase):
|
|||
user = User.objects.create_user('testuser', 'test@example.com', 'password123')
|
||||
profile = UserProfile.objects.all().filter(user_id=user.id).first()
|
||||
self.assertIsNotNone(profile)
|
||||
|
||||
def test_bookmark_sharing_is_disabled_by_default(self):
|
||||
user = User.objects.create_user('testuser', 'test@example.com', 'password123')
|
||||
profile = UserProfile.objects.all().filter(user_id=user.id).first()
|
||||
self.assertFalse(profile.enable_sharing)
|
||||
|
|
76
bookmarks/tests/test_user_select_tag.py
Normal file
76
bookmarks/tests/test_user_select_tag.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
from django.db.models import QuerySet
|
||||
from django.template import Template, RequestContext
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
from bookmarks.models import BookmarkFilters, User
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class UserSelectTagTest(TestCase, BookmarkFactoryMixin):
|
||||
def render_template(self, url: str, users: QuerySet[User] = User.objects.all()):
|
||||
rf = RequestFactory()
|
||||
request = rf.get(url)
|
||||
filters = BookmarkFilters(request)
|
||||
context = RequestContext(request, {
|
||||
'request': request,
|
||||
'filters': filters,
|
||||
'users': users,
|
||||
})
|
||||
template_to_render = Template(
|
||||
'{% load bookmarks %}'
|
||||
'{% user_select filters users %}'
|
||||
)
|
||||
return template_to_render.render(context)
|
||||
|
||||
def assertUserOption(self, html: str, user: User, selected: bool = False):
|
||||
self.assertInHTML(f'''
|
||||
<option value="{user.username}"
|
||||
{'selected' if selected else ''}
|
||||
data-is-user-option>
|
||||
{user.username}
|
||||
</option>
|
||||
''', html)
|
||||
|
||||
def test_empty_option(self):
|
||||
rendered_template = self.render_template('/test')
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<option value="">Everyone</option>
|
||||
''', rendered_template)
|
||||
|
||||
def test_render_user_options(self):
|
||||
user1 = User.objects.create_user('user1', 'user1@example.com', 'password123')
|
||||
user2 = User.objects.create_user('user2', 'user2@example.com', 'password123')
|
||||
user3 = User.objects.create_user('user3', 'user3@example.com', 'password123')
|
||||
|
||||
rendered_template = self.render_template('/test', User.objects.all())
|
||||
|
||||
self.assertUserOption(rendered_template, user1)
|
||||
self.assertUserOption(rendered_template, user2)
|
||||
self.assertUserOption(rendered_template, user3)
|
||||
|
||||
def test_preselect_user_option(self):
|
||||
user1 = User.objects.create_user('user1', 'user1@example.com', 'password123')
|
||||
User.objects.create_user('user2', 'user2@example.com', 'password123')
|
||||
User.objects.create_user('user3', 'user3@example.com', 'password123')
|
||||
|
||||
rendered_template = self.render_template('/test?user=user1', User.objects.all())
|
||||
|
||||
self.assertUserOption(rendered_template, user1, True)
|
||||
|
||||
def test_render_hidden_inputs_for_filter_params(self):
|
||||
# Should render hidden inputs if query param exists
|
||||
url = '/test?q=foo&user=john'
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertInHTML('''
|
||||
<input type="hidden" name="q" value="foo">
|
||||
''', rendered_template)
|
||||
|
||||
# Should not render hidden inputs if query param does not exist
|
||||
url = '/test?user=john'
|
||||
rendered_template = self.render_template(url)
|
||||
|
||||
self.assertInHTML('''
|
||||
<input type="hidden" name="q" value="foo">
|
||||
''', rendered_template, count=0)
|
|
@ -13,6 +13,7 @@ urlpatterns = [
|
|||
# Bookmarks
|
||||
path('bookmarks', views.bookmarks.index, name='index'),
|
||||
path('bookmarks/archived', views.bookmarks.archived, name='archived'),
|
||||
path('bookmarks/shared', views.bookmarks.shared, name='shared'),
|
||||
path('bookmarks/new', views.bookmarks.new, name='new'),
|
||||
path('bookmarks/close', views.bookmarks.close, name='close'),
|
||||
path('bookmarks/<int:bookmark_id>/edit', views.bookmarks.edit, name='edit'),
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import urllib.parse
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import QuerySet, prefetch_related_objects
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks.models import Bookmark, BookmarkForm, build_tag_string
|
||||
from bookmarks.models import Bookmark, BookmarkForm, BookmarkFilters, User, Tag, build_tag_string
|
||||
from bookmarks.services.bookmarks import create_bookmark, update_bookmark, archive_bookmark, archive_bookmarks, \
|
||||
unarchive_bookmark, unarchive_bookmarks, delete_bookmarks, tag_bookmarks, untag_bookmarks
|
||||
from bookmarks.utils import get_safe_return_url
|
||||
|
@ -17,30 +19,48 @@ _default_page_size = 30
|
|||
|
||||
@login_required
|
||||
def index(request):
|
||||
query_string = request.GET.get('q')
|
||||
query_set = queries.query_bookmarks(request.user, query_string)
|
||||
tags = queries.query_bookmark_tags(request.user, query_string)
|
||||
filters = BookmarkFilters(request)
|
||||
query_set = queries.query_bookmarks(request.user, filters.query)
|
||||
tags = queries.query_bookmark_tags(request.user, filters.query)
|
||||
base_url = reverse('bookmarks:index')
|
||||
context = get_bookmark_view_context(request, query_set, tags, base_url)
|
||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||
return render(request, 'bookmarks/index.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def archived(request):
|
||||
query_string = request.GET.get('q')
|
||||
query_set = queries.query_archived_bookmarks(request.user, query_string)
|
||||
tags = queries.query_archived_bookmark_tags(request.user, query_string)
|
||||
filters = BookmarkFilters(request)
|
||||
query_set = queries.query_archived_bookmarks(request.user, filters.query)
|
||||
tags = queries.query_archived_bookmark_tags(request.user, filters.query)
|
||||
base_url = reverse('bookmarks:archived')
|
||||
context = get_bookmark_view_context(request, query_set, tags, base_url)
|
||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||
return render(request, 'bookmarks/archive.html', context)
|
||||
|
||||
|
||||
def get_bookmark_view_context(request, query_set, tags, base_url):
|
||||
@login_required
|
||||
def shared(request):
|
||||
filters = BookmarkFilters(request)
|
||||
user = User.objects.filter(username=filters.user).first()
|
||||
query_set = queries.query_shared_bookmarks(user, filters.query)
|
||||
tags = queries.query_shared_bookmark_tags(user, filters.query)
|
||||
users = queries.query_shared_bookmark_users(filters.query)
|
||||
base_url = reverse('bookmarks:shared')
|
||||
context = get_bookmark_view_context(request, filters, query_set, tags, base_url)
|
||||
context['users'] = users
|
||||
return render(request, 'bookmarks/shared.html', context)
|
||||
|
||||
|
||||
def get_bookmark_view_context(request: WSGIRequest,
|
||||
filters: BookmarkFilters,
|
||||
query_set: QuerySet[Bookmark],
|
||||
tags: QuerySet[Tag],
|
||||
base_url: str):
|
||||
page = request.GET.get('page')
|
||||
query_string = request.GET.get('q')
|
||||
paginator = Paginator(query_set, _default_page_size)
|
||||
bookmarks = paginator.get_page(page)
|
||||
return_url = generate_return_url(base_url, page, query_string)
|
||||
# Prefetch owner relation, this avoids n+1 queries when using the owner in templates
|
||||
prefetch_related_objects(bookmarks.object_list, 'owner')
|
||||
return_url = generate_return_url(base_url, page, filters)
|
||||
link_target = request.user.profile.bookmark_link_target
|
||||
|
||||
if request.GET.get('tag'):
|
||||
|
@ -51,17 +71,19 @@ def get_bookmark_view_context(request, query_set, tags, base_url):
|
|||
return {
|
||||
'bookmarks': bookmarks,
|
||||
'tags': tags,
|
||||
'query': query_string if query_string else '',
|
||||
'filters': filters,
|
||||
'empty': paginator.count == 0,
|
||||
'return_url': return_url,
|
||||
'link_target': link_target,
|
||||
}
|
||||
|
||||
|
||||
def generate_return_url(base_url, page, query_string):
|
||||
def generate_return_url(base_url: str, page: int, filters: BookmarkFilters):
|
||||
url_query = {}
|
||||
if query_string is not None:
|
||||
url_query['q'] = query_string
|
||||
if filters.query:
|
||||
url_query['q'] = filters.query
|
||||
if filters.user:
|
||||
url_query['user'] = filters.user
|
||||
if page is not None:
|
||||
url_query['page'] = page
|
||||
url_params = urllib.parse.urlencode(url_query)
|
||||
|
|
Loading…
Reference in a new issue