mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-15 06:29:21 +02:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e1c9a7add6 | ||
![]() |
82b4268a26 | ||
![]() |
5287eb3f8b | ||
![]() |
d298260122 | ||
![]() |
12e5810aee | ||
![]() |
1dabd0266b | ||
![]() |
7390fc3f4f | ||
![]() |
5e003ede92 | ||
![]() |
984eef92e2 | ||
![]() |
eae6ca6e07 | ||
![]() |
a6bfaa7c78 | ||
![]() |
7aa1630be2 | ||
![]() |
4f9fcb41bd | ||
![]() |
da4a81305a | ||
![]() |
df33144dd0 |
@@ -8,6 +8,7 @@
|
||||
/static
|
||||
/build
|
||||
/out
|
||||
/.git
|
||||
|
||||
/.dockerignore
|
||||
/.gitignore
|
||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## v1.8.4 (16/10/2021)
|
||||
- [**enhancement**] Allow non-admin users to change their password [#166](https://github.com/sissbruecker/linkding/issues/166)
|
||||
|
||||
---
|
||||
|
||||
## v1.8.3 (03/10/2021)
|
||||
- [**enhancement**] Enhancement: let user configure to open links in same tab instead on a new window/tab [#27](https://github.com/sissbruecker/linkding/issues/27)
|
||||
|
||||
---
|
||||
|
||||
## v1.8.2 (02/10/2021)
|
||||
- [**bug**] Fix jumping search box [#163](https://github.com/sissbruecker/linkding/pull/163)
|
||||
---
|
||||
|
||||
## v1.8.1 (01/10/2021)
|
||||
- [**enhancement**] Add global shortcut for search [#161](https://github.com/sissbruecker/linkding/pull/161)
|
||||
- allows to press `s` to focus the search input
|
||||
|
@@ -58,6 +58,8 @@ class AdminTag(admin.ModelAdmin):
|
||||
|
||||
def bookmarks_count(self, obj):
|
||||
return obj.bookmarks_count
|
||||
|
||||
bookmarks_count.admin_order_field = 'bookmarks_count'
|
||||
|
||||
def delete_unused_tags(self, request, queryset: QuerySet):
|
||||
unused_tags = queryset.filter(bookmark__isnull=True)
|
||||
|
@@ -41,14 +41,14 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
bookmark.url = validated_data['url']
|
||||
bookmark.title = validated_data['title']
|
||||
bookmark.description = validated_data['description']
|
||||
tag_string = build_tag_string(validated_data['tag_names'], ' ')
|
||||
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):
|
||||
instance.url = validated_data['url']
|
||||
instance.title = validated_data['title']
|
||||
instance.description = validated_data['description']
|
||||
tag_string = build_tag_string(validated_data['tag_names'], ' ')
|
||||
tag_string = build_tag_string(validated_data['tag_names'])
|
||||
return update_bookmark(instance, tag_string, self.context['user'])
|
||||
|
||||
|
||||
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.6 on 2021-10-03 06:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookmarks', '0009_bookmark_web_archive_snapshot_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userprofile',
|
||||
name='bookmark_link_target',
|
||||
field=models.CharField(choices=[('_blank', 'New page'), ('_self', 'Same page')], default='_blank', max_length=10),
|
||||
),
|
||||
]
|
@@ -20,11 +20,19 @@ class Tag(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
def sanitize_tag_name(tag_name: str):
|
||||
# strip leading/trailing spaces
|
||||
# replace inner spaces with replacement char
|
||||
return tag_name.strip().replace(' ', '-')
|
||||
|
||||
|
||||
def parse_tag_string(tag_string: str, delimiter: str = ','):
|
||||
if not tag_string:
|
||||
return []
|
||||
names = tag_string.strip().split(delimiter)
|
||||
names = [name.strip() for name in names if name]
|
||||
# remove empty names, sanitize remaining names
|
||||
names = [sanitize_tag_name(name) for name in names if name]
|
||||
# remove duplicates
|
||||
names = unique(names, str.lower)
|
||||
names.sort(key=str.lower)
|
||||
|
||||
@@ -116,16 +124,24 @@ class UserProfile(models.Model):
|
||||
(BOOKMARK_DATE_DISPLAY_ABSOLUTE, 'Absolute'),
|
||||
(BOOKMARK_DATE_DISPLAY_HIDDEN, 'Hidden'),
|
||||
]
|
||||
BOOKMARK_LINK_TARGET_BLANK = '_blank'
|
||||
BOOKMARK_LINK_TARGET_SELF = '_self'
|
||||
BOOKMARK_LINK_TARGET_CHOICES = [
|
||||
(BOOKMARK_LINK_TARGET_BLANK, 'New page'),
|
||||
(BOOKMARK_LINK_TARGET_SELF, 'Same page'),
|
||||
]
|
||||
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE)
|
||||
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO)
|
||||
bookmark_date_display = models.CharField(max_length=10, choices=BOOKMARK_DATE_DISPLAY_CHOICES, blank=False,
|
||||
default=BOOKMARK_DATE_DISPLAY_RELATIVE)
|
||||
bookmark_link_target = models.CharField(max_length=10, choices=BOOKMARK_LINK_TARGET_CHOICES, blank=False,
|
||||
default=BOOKMARK_LINK_TARGET_BLANK)
|
||||
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['theme', 'bookmark_date_display']
|
||||
fields = ['theme', 'bookmark_date_display', 'bookmark_link_target']
|
||||
|
||||
|
||||
@receiver(post_save, sender=get_user_model())
|
||||
|
@@ -90,7 +90,7 @@ def delete_bookmarks(bookmark_ids: [Union[int, str]], current_user: User):
|
||||
def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
tag_names = parse_tag_string(tag_string, ' ')
|
||||
tag_names = parse_tag_string(tag_string)
|
||||
tags = get_or_create_tags(tag_names, current_user)
|
||||
|
||||
for bookmark in bookmarks:
|
||||
@@ -103,7 +103,7 @@ def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user
|
||||
def untag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_bookmark_ids)
|
||||
tag_names = parse_tag_string(tag_string, ' ')
|
||||
tag_names = parse_tag_string(tag_string)
|
||||
tags = get_or_create_tags(tag_names, current_user)
|
||||
|
||||
for bookmark in bookmarks:
|
||||
@@ -125,7 +125,7 @@ def _update_website_metadata(bookmark: Bookmark):
|
||||
|
||||
|
||||
def _update_bookmark_tags(bookmark: Bookmark, tag_string: str, user: User):
|
||||
tag_names = parse_tag_string(tag_string, ' ')
|
||||
tag_names = parse_tag_string(tag_string)
|
||||
tags = get_or_create_tags(tag_names, user)
|
||||
bookmark.tags.set(tags)
|
||||
|
||||
|
@@ -26,7 +26,7 @@
|
||||
{% if empty %}
|
||||
{% include 'bookmarks/empty_bookmarks.html' %}
|
||||
{% else %}
|
||||
{% bookmark_list bookmarks return_url %}
|
||||
{% bookmark_list bookmarks return_url link_target %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</section>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
<div class="title truncate">
|
||||
<a href="{{ bookmark.url }}" target="_blank" rel="noopener">{{ bookmark.resolved_title }}</a>
|
||||
<a href="{{ bookmark.url }}" target="{{ link_target }}" rel="noopener">{{ bookmark.resolved_title }}</a>
|
||||
</div>
|
||||
<div class="description truncate">
|
||||
{% if bookmark.tag_names %}
|
||||
@@ -30,7 +30,7 @@
|
||||
<span class="date-label text-gray text-sm">
|
||||
{% if bookmark.web_archive_snapshot_url %}
|
||||
<a href="{{ bookmark.web_archive_snapshot_url }}"
|
||||
title="Show snapshot on web archive" target="_blank" rel="noopener">
|
||||
title="Show snapshot on web archive" target="{{ link_target }}" rel="noopener">
|
||||
{% endif %}
|
||||
<span>{{ bookmark.date_added|humanize_relative_date }}</span>
|
||||
{% if bookmark.web_archive_snapshot_url %}
|
||||
@@ -44,7 +44,7 @@
|
||||
<span class="date-label text-gray text-sm">
|
||||
{% if bookmark.web_archive_snapshot_url %}
|
||||
<a href="{{ bookmark.web_archive_snapshot_url }}"
|
||||
title="Show snapshot on web archive" target="_blank" rel="noopener">
|
||||
title="Show snapshot on web archive" target="{{ link_target }}" rel="noopener">
|
||||
{% endif %}
|
||||
<span>{{ bookmark.date_added|humanize_absolute_date }}</span>
|
||||
{% if bookmark.web_archive_snapshot_url %}
|
||||
|
@@ -26,7 +26,7 @@
|
||||
{% if empty %}
|
||||
{% include 'bookmarks/empty_bookmarks.html' %}
|
||||
{% else %}
|
||||
{% bookmark_list bookmarks return_url %}
|
||||
{% bookmark_list bookmarks return_url link_target %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</section>
|
||||
|
21
bookmarks/templates/registration/password_change_done.html
Normal file
21
bookmarks/templates/registration/password_change_done.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Password changed{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="auth-page">
|
||||
<div class="columns">
|
||||
<section class="content-area column col-5 col-md-12">
|
||||
<div class="content-area-header">
|
||||
<h2>Password Changed</h2>
|
||||
</div>
|
||||
<p class="text-success">
|
||||
Your password was changed successfully.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
55
bookmarks/templates/registration/password_change_form.html
Normal file
55
bookmarks/templates/registration/password_change_form.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}Change Password{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="auth-page">
|
||||
<div class="columns">
|
||||
<section class="content-area column col-5 col-md-12">
|
||||
<div class="content-area-header">
|
||||
<h2>Change Password</h2>
|
||||
</div>
|
||||
<form method="post" action="{% url 'change_password' %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group {% if form.old_password.errors %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.old_password.id_for_label }}">Old password</label>
|
||||
{{ form.old_password|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.old_password.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.old_password.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group {% if form.new_password1.errors %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.new_password1.id_for_label }}">New password</label>
|
||||
{{ form.new_password1|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.new_password1.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.new_password1.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group {% if form.new_password2.errors %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.new_password2.id_for_label }}">Confirm new password</label>
|
||||
{{ form.new_password2|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.new_password2.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.new_password2.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div class="columns">
|
||||
<div class="column col-3">
|
||||
<input type="submit" value="Change Password" class="btn btn-primary">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@@ -1,25 +0,0 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="settings-page">
|
||||
|
||||
{% include 'settings/nav.html' %}
|
||||
|
||||
<section class="content-area">
|
||||
<h2>API Token</h2>
|
||||
<p>The following token can be used to authenticate 3rd-party applications against the REST API:</p>
|
||||
<div class="form-group">
|
||||
<div class="columns">
|
||||
<div class="column col-6 col-md-12">
|
||||
<input class="form-input" value="{{ api_token }}" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>Please treat this token as you would any other credential.</strong> Any party with access to this
|
||||
token can access and manage all your bookmarks.</p>
|
||||
<p>If you think that a token was compromised you can revoke (delete) it in the <a href="{% url 'admin:authtoken_tokenproxy_changelist' %}">admin panel</a>. After deleting the token, a new one will be generated when you reload this settings page.</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@@ -9,6 +9,9 @@
|
||||
{# Profile section #}
|
||||
<section class="content-area">
|
||||
<h2>Profile</h2>
|
||||
<p>
|
||||
<a href="{% url 'change_password' %}">Change password</a>
|
||||
</p>
|
||||
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
@@ -19,6 +22,10 @@
|
||||
<label for="{{ form.bookmark_date_display.id_for_label }}" class="form-label">Bookmark date format</label>
|
||||
{{ form.bookmark_date_display|add_class:"form-select col-2 col-sm-12" }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.bookmark_link_target.id_for_label }}" class="form-label">Open bookmarks in</label>
|
||||
{{ form.bookmark_link_target|add_class:"form-select col-2 col-sm-12" }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Save" class="btn btn-primary mt-2">
|
||||
</div>
|
||||
|
@@ -5,7 +5,6 @@
|
||||
|
||||
{% include 'settings/nav.html' %}
|
||||
|
||||
{# Integrations section #}
|
||||
<section class="content-area">
|
||||
<h2>Browser Extension</h2>
|
||||
<p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The extension is available in the official extension stores for:</p>
|
||||
@@ -29,5 +28,19 @@
|
||||
class="btn btn-primary">📎 Add bookmark</a>
|
||||
</section>
|
||||
|
||||
<section class="content-area">
|
||||
<h2>REST API</h2>
|
||||
<p>The following token can be used to authenticate 3rd-party applications against the REST API:</p>
|
||||
<div class="form-group">
|
||||
<div class="columns">
|
||||
<div class="column col-6 col-md-12">
|
||||
<input class="form-input" value="{{ api_token }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>Please treat this token as you would any other credential.</strong> Any party with access to this
|
||||
token can access and manage all your bookmarks.</p>
|
||||
<p>If you think that a token was compromised you can revoke (delete) it in the <a href="{% url 'admin:authtoken_tokenproxy_changelist' %}">admin panel</a>. After deleting the token, a new one will be generated when you reload this settings page.</p>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
{% url 'bookmarks:settings.index' as index_url %}
|
||||
{% url 'bookmarks:settings.general' as general_url %}
|
||||
{% url 'bookmarks:settings.integrations' as integrations_url %}
|
||||
{% url 'bookmarks:settings.api' as api_url %}
|
||||
|
||||
<ul class="tab tab-block">
|
||||
<li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == general_url%}active{% endif %}">
|
||||
@@ -10,9 +9,6 @@
|
||||
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
|
||||
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
|
||||
</li>
|
||||
<li class="tab-item {% if request.get_full_path == api_url %}active{% endif %}">
|
||||
<a href="{% url 'bookmarks:settings.api' %}">API</a>
|
||||
</li>
|
||||
<li class="tab-item tooltip tooltip-bottom" data-tooltip="The admin panel provides additional features 
 such as user management and bulk operations.">
|
||||
<a href="{% url 'admin:index' %}" target="_blank">
|
||||
<span>Admin</span>
|
||||
|
@@ -51,11 +51,12 @@ def tag_cloud(context, tags: List[Tag]):
|
||||
|
||||
|
||||
@register.inclusion_tag('bookmarks/bookmark_list.html', name='bookmark_list', takes_context=True)
|
||||
def bookmark_list(context, bookmarks: Page, return_url: str):
|
||||
def bookmark_list(context, bookmarks: Page, return_url: str, link_target: str = '_blank'):
|
||||
return {
|
||||
'request': context['request'],
|
||||
'bookmarks': bookmarks,
|
||||
'return_url': return_url
|
||||
'return_url': return_url,
|
||||
'link_target': link_target,
|
||||
}
|
||||
|
||||
|
||||
|
@@ -2,7 +2,7 @@ from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import Bookmark, Tag
|
||||
from bookmarks.models import Bookmark, Tag, UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
@@ -12,22 +12,22 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertVisibleBookmarks(self, response, bookmarks: [Bookmark]):
|
||||
def assertVisibleBookmarks(self, response, bookmarks: [Bookmark], link_target: str = '_blank'):
|
||||
html = response.content.decode()
|
||||
self.assertContains(response, 'data-is-bookmark-item', count=len(bookmarks))
|
||||
|
||||
for bookmark in bookmarks:
|
||||
self.assertInHTML(
|
||||
'<a href="{0}" target="_blank" rel="noopener">{1}</a>'.format(bookmark.url, bookmark.resolved_title),
|
||||
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
|
||||
html
|
||||
)
|
||||
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: [Bookmark]):
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: [Bookmark], link_target: str = '_blank'):
|
||||
html = response.content.decode()
|
||||
|
||||
for bookmark in bookmarks:
|
||||
self.assertInHTML(
|
||||
'<a href="{0}" target="_blank" rel="noopener">{1}</a>'.format(bookmark.url, bookmark.resolved_title),
|
||||
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
|
||||
html,
|
||||
count=0
|
||||
)
|
||||
@@ -130,3 +130,29 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(is_archived=True)
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
|
||||
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(is_archived=True),
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(is_archived=True)
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
|
||||
|
@@ -2,7 +2,7 @@ from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import Bookmark, Tag
|
||||
from bookmarks.models import Bookmark, Tag, UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
@@ -12,22 +12,22 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertVisibleBookmarks(self, response, bookmarks: [Bookmark]):
|
||||
def assertVisibleBookmarks(self, response, bookmarks: [Bookmark], link_target: str = '_blank'):
|
||||
html = response.content.decode()
|
||||
self.assertContains(response, 'data-is-bookmark-item', count=len(bookmarks))
|
||||
|
||||
for bookmark in bookmarks:
|
||||
self.assertInHTML(
|
||||
'<a href="{0}" target="_blank" rel="noopener">{1}</a>'.format(bookmark.url, bookmark.resolved_title),
|
||||
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
|
||||
html
|
||||
)
|
||||
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: [Bookmark]):
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: [Bookmark], link_target: str = '_blank'):
|
||||
html = response.content.decode()
|
||||
|
||||
for bookmark in bookmarks:
|
||||
self.assertInHTML(
|
||||
'<a href="{0}" target="_blank" rel="noopener">{1}</a>'.format(bookmark.url, bookmark.resolved_title),
|
||||
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
|
||||
html,
|
||||
count=0
|
||||
)
|
||||
@@ -130,3 +130,29 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_open_bookmarks_in_new_page_by_default(self):
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark()
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
|
||||
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(),
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark()
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks, '_self')
|
||||
|
@@ -60,6 +60,18 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
|
||||
|
||||
def test_create_bookmark_replaces_whitespace_in_tag_names(self):
|
||||
data = {
|
||||
'url': 'https://example.com/',
|
||||
'title': 'Test title',
|
||||
'description': 'Test description',
|
||||
'tag_names': ['tag 1', 'tag 2']
|
||||
}
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
bookmark = Bookmark.objects.get(url=data['url'])
|
||||
tag_names = [tag.name for tag in bookmark.tags.all()]
|
||||
self.assertListEqual(tag_names, ['tag-1', 'tag-2'])
|
||||
|
||||
def test_create_bookmark_minimal_payload(self):
|
||||
data = {'url': 'https://example.com/'}
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
|
@@ -4,13 +4,39 @@ from django.template import Template, RequestContext
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.utils import timezone, formats
|
||||
|
||||
from bookmarks.models import UserProfile
|
||||
from bookmarks.models import Bookmark, UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def render_template(self, bookmarks) -> str:
|
||||
def assertBookmarksLink(self, html: str, bookmark: Bookmark, link_target: str = '_blank'):
|
||||
self.assertInHTML(
|
||||
f'<a href="{bookmark.url}" target="{link_target}" rel="noopener">{bookmark.resolved_title}</a>',
|
||||
html
|
||||
)
|
||||
|
||||
def assertDateLabel(self, html: str, label_content: str):
|
||||
self.assertInHTML(f'''
|
||||
<span class="date-label text-gray text-sm">
|
||||
<span>{label_content}</span>
|
||||
</span>
|
||||
<span class="text-gray text-sm">|</span>
|
||||
''', html)
|
||||
|
||||
def assertWebArchiveLink(self, html: str, label_content: str, url: str, link_target: str = '_blank'):
|
||||
self.assertInHTML(f'''
|
||||
<span class="date-label text-gray text-sm">
|
||||
<a href="{url}"
|
||||
title="Show snapshot on web archive" target="{link_target}" rel="noopener">
|
||||
<span>{label_content}</span>
|
||||
<span>∞</span>
|
||||
</a>
|
||||
</span>
|
||||
<span class="text-gray text-sm">|</span>
|
||||
''', html)
|
||||
|
||||
def render_template(self, bookmarks: [Bookmark], template: Template) -> str:
|
||||
rf = RequestFactory()
|
||||
request = rf.get('/test')
|
||||
request.user = self.get_or_create_test_user()
|
||||
@@ -18,11 +44,23 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||
page = paginator.page(1)
|
||||
|
||||
context = RequestContext(request, {'bookmarks': page, 'return_url': '/test'})
|
||||
template_to_render = Template(
|
||||
return template.render(context)
|
||||
|
||||
def render_default_template(self, bookmarks: [Bookmark]) -> str:
|
||||
template = Template(
|
||||
'{% load bookmarks %}'
|
||||
'{% bookmark_list bookmarks return_url %}'
|
||||
)
|
||||
return template_to_render.render(context)
|
||||
return self.render_template(bookmarks, template)
|
||||
|
||||
def render_template_with_link_target(self, bookmarks: [Bookmark], link_target: str) -> str:
|
||||
template = Template(
|
||||
f'''
|
||||
{{% load bookmarks %}}
|
||||
{{% bookmark_list bookmarks return_url '{link_target}' %}}
|
||||
'''
|
||||
)
|
||||
return self.render_template(bookmarks, template)
|
||||
|
||||
def setup_date_format_test(self, date_display_setting: str, web_archive_url: str = ''):
|
||||
bookmark = self.setup_bookmark()
|
||||
@@ -36,55 +74,62 @@ class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_should_respect_absolute_date_setting(self):
|
||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE)
|
||||
html = self.render_template([bookmark])
|
||||
html = self.render_default_template([bookmark])
|
||||
formatted_date = formats.date_format(bookmark.date_added, 'SHORT_DATE_FORMAT')
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<span class="date-label text-gray text-sm">
|
||||
<span>{formatted_date}</span>
|
||||
</span>
|
||||
<span class="text-gray text-sm">|</span>
|
||||
''', html)
|
||||
self.assertDateLabel(html, formatted_date)
|
||||
|
||||
def test_should_render_web_archive_link_with_absolute_date_setting(self):
|
||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_ABSOLUTE,
|
||||
'https://web.archive.org/web/20210811214511/https://wanikani.com/')
|
||||
html = self.render_template([bookmark])
|
||||
html = self.render_default_template([bookmark])
|
||||
formatted_date = formats.date_format(bookmark.date_added, 'SHORT_DATE_FORMAT')
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<span class="date-label text-gray text-sm">
|
||||
<a href="{bookmark.web_archive_snapshot_url}"
|
||||
title="Show snapshot on web archive" target="_blank" rel="noopener">
|
||||
<span>{formatted_date}</span>
|
||||
<span>∞</span>
|
||||
</a>
|
||||
</span>
|
||||
<span class="text-gray text-sm">|</span>
|
||||
''', html)
|
||||
self.assertWebArchiveLink(html, formatted_date, bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_should_respect_relative_date_setting(self):
|
||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE)
|
||||
html = self.render_template([bookmark])
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertInHTML('''
|
||||
<span class="date-label text-gray text-sm">
|
||||
<span>1 week ago</span>
|
||||
</span>
|
||||
<span class="text-gray text-sm">|</span>
|
||||
''', html)
|
||||
self.assertDateLabel(html, '1 week ago')
|
||||
|
||||
def test_should_render_web_archive_link_with_relative_date_setting(self):
|
||||
bookmark = self.setup_date_format_test(UserProfile.BOOKMARK_DATE_DISPLAY_RELATIVE,
|
||||
'https://web.archive.org/web/20210811214511/https://wanikani.com/')
|
||||
html = self.render_template([bookmark])
|
||||
self.assertInHTML(f'''
|
||||
<span class="date-label text-gray text-sm">
|
||||
<a href="{bookmark.web_archive_snapshot_url}"
|
||||
title="Show snapshot on web archive" target="_blank" rel="noopener">
|
||||
<span>1 week ago</span>
|
||||
<span>∞</span>
|
||||
</a>
|
||||
</span>
|
||||
<span class="text-gray text-sm">|</span>
|
||||
''', html)
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url)
|
||||
|
||||
def test_bookmark_link_target_should_be_blank_by_default(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertBookmarksLink(html, bookmark, link_target='_blank')
|
||||
|
||||
def test_bookmark_link_target_should_respect_link_target_parameter(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
html = self.render_template_with_link_target([bookmark], '_self')
|
||||
|
||||
self.assertBookmarksLink(html, bookmark, link_target='_self')
|
||||
|
||||
def test_web_archive_link_target_should_be_blank_by_default(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
||||
bookmark.web_archive_snapshot_url = 'https://example.com'
|
||||
bookmark.save()
|
||||
|
||||
html = self.render_default_template([bookmark])
|
||||
|
||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_blank')
|
||||
|
||||
def test_web_archive_link_target_respect_link_target_parameter(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.date_added = timezone.now() - relativedelta(days=8)
|
||||
bookmark.web_archive_snapshot_url = 'https://example.com'
|
||||
bookmark.save()
|
||||
|
||||
html = self.render_template_with_link_target([bookmark], '_self')
|
||||
|
||||
self.assertWebArchiveLink(html, '1 week ago', bookmark.web_archive_snapshot_url, link_target='_self')
|
||||
|
@@ -21,7 +21,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_create_should_create_web_archive_snapshot(self):
|
||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||
bookmark_data = Bookmark(url='https://example.com')
|
||||
bookmark = create_bookmark(bookmark_data, 'tag1 tag2', self.user)
|
||||
bookmark = create_bookmark(bookmark_data, 'tag1,tag2', self.user)
|
||||
|
||||
mock_create_web_archive_snapshot.assert_called_once_with(bookmark.id, False)
|
||||
|
||||
@@ -29,7 +29,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.url = 'https://example.com/updated'
|
||||
update_bookmark(bookmark, 'tag1 tag2', self.user)
|
||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
||||
|
||||
mock_create_web_archive_snapshot.assert_called_once_with(bookmark.id, True)
|
||||
|
||||
@@ -37,7 +37,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||
bookmark = self.setup_bookmark()
|
||||
bookmark.title = 'updated title'
|
||||
update_bookmark(bookmark, 'tag1 tag2', self.user)
|
||||
update_bookmark(bookmark, 'tag1,tag2', self.user)
|
||||
|
||||
mock_create_web_archive_snapshot.assert_not_called()
|
||||
|
||||
@@ -216,7 +216,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name} {tag2.name}',
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
@@ -232,7 +232,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark()
|
||||
bookmark3 = self.setup_bookmark()
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], 'tag1 tag2', self.get_or_create_test_user())
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], 'tag1,tag2', self.get_or_create_test_user())
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -257,7 +257,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark3.id], f'{tag1.name} {tag2.name}', self.get_or_create_test_user())
|
||||
tag_bookmarks([bookmark1.id, bookmark3.id], f'{tag1.name},{tag2.name}', self.get_or_create_test_user())
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -275,7 +275,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name} {tag2.name}',
|
||||
tag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
@@ -293,7 +293,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
|
||||
tag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name} {tag2.name}',
|
||||
tag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
|
||||
self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2])
|
||||
@@ -307,7 +307,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
|
||||
untag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name} {tag2.name}',
|
||||
untag_bookmarks([bookmark1.id, bookmark2.id, bookmark3.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
@@ -325,7 +325,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
|
||||
untag_bookmarks([bookmark1.id, bookmark3.id], f'{tag1.name} {tag2.name}', self.get_or_create_test_user())
|
||||
untag_bookmarks([bookmark1.id, bookmark3.id], f'{tag1.name},{tag2.name}', self.get_or_create_test_user())
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
bookmark2.refresh_from_db()
|
||||
@@ -343,7 +343,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
inaccessible_bookmark = self.setup_bookmark(user=other_user, tags=[tag1, tag2])
|
||||
|
||||
untag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name} {tag2.name}',
|
||||
untag_bookmarks([bookmark1.id, bookmark2.id, inaccessible_bookmark.id], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
|
||||
bookmark1.refresh_from_db()
|
||||
@@ -361,7 +361,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
bookmark3 = self.setup_bookmark(tags=[tag1, tag2])
|
||||
|
||||
untag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name} {tag2.name}',
|
||||
untag_bookmarks([str(bookmark1.id), bookmark2.id, str(bookmark3.id)], f'{tag1.name},{tag2.name}',
|
||||
self.get_or_create_test_user())
|
||||
|
||||
self.assertCountEqual(bookmark1.tags.all(), [])
|
||||
|
@@ -2,6 +2,7 @@ from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from bookmarks.models import Tag
|
||||
from bookmarks.services import tasks
|
||||
from bookmarks.services.importer import import_netscape_html
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging
|
||||
@@ -20,6 +21,18 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin):
|
||||
</DL><p>
|
||||
'''
|
||||
|
||||
def test_replace_whitespace_in_tag_names(self):
|
||||
test_html = self.create_import_html(f'''
|
||||
<DT><A HREF="https://example.com" ADD_DATE="1616337559" PRIVATE="0" TOREAD="0" TAGS="tag 1, tag 2, tag 3">Example.com</A>
|
||||
<DD>Example.com
|
||||
''')
|
||||
import_netscape_html(test_html, self.get_or_create_test_user())
|
||||
|
||||
tags = Tag.objects.all()
|
||||
tag_names = [tag.name for tag in tags]
|
||||
|
||||
self.assertListEqual(tag_names, ['tag-1', 'tag-2', 'tag-3'])
|
||||
|
||||
@disable_logging
|
||||
def test_validate_empty_or_missing_bookmark_url(self):
|
||||
test_html = self.create_import_html(f'''
|
||||
|
55
bookmarks/tests/test_password_change_view.py
Normal file
55
bookmarks/tests/test_password_change_view.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class PasswordChangeViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_user('testuser', 'test@example.com', 'initial_password')
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_change_password(self):
|
||||
form_data = {
|
||||
'old_password': 'initial_password',
|
||||
'new_password1': 'new_password',
|
||||
'new_password2': 'new_password',
|
||||
}
|
||||
|
||||
response = self.client.post(reverse('change_password'), form_data)
|
||||
|
||||
self.assertRedirects(response, reverse('password_change_done'))
|
||||
|
||||
def test_change_password_done(self):
|
||||
form_data = {
|
||||
'old_password': 'initial_password',
|
||||
'new_password1': 'new_password',
|
||||
'new_password2': 'new_password',
|
||||
}
|
||||
|
||||
response = self.client.post(reverse('change_password'), form_data, follow=True)
|
||||
|
||||
self.assertContains(response, 'Your password was changed successfully')
|
||||
|
||||
def test_should_return_error_for_invalid_old_password(self):
|
||||
form_data = {
|
||||
'old_password': 'wrong_password',
|
||||
'new_password1': 'new_password',
|
||||
'new_password2': 'new_password',
|
||||
}
|
||||
|
||||
response = self.client.post(reverse('change_password'), form_data)
|
||||
|
||||
self.assertIn('old_password', response.context_data['form'].errors)
|
||||
|
||||
def test_should_return_error_for_mismatching_new_password(self):
|
||||
form_data = {
|
||||
'old_password': 'initial_password',
|
||||
'new_password1': 'new_password',
|
||||
'new_password2': 'wrong_password',
|
||||
}
|
||||
|
||||
response = self.client.post(reverse('change_password'), form_data)
|
||||
|
||||
self.assertIn('new_password2', response.context_data['form'].errors)
|
@@ -1,40 +0,0 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class SettingsApiViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def test_should_render_successfully(self):
|
||||
response = self.client.get(reverse('bookmarks:settings.api'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_should_check_authentication(self):
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse('bookmarks:settings.api'), follow=True)
|
||||
|
||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.api'))
|
||||
|
||||
def test_should_generate_api_token_if_not_exists(self):
|
||||
self.assertEqual(Token.objects.count(), 0)
|
||||
|
||||
self.client.get(reverse('bookmarks:settings.api'))
|
||||
|
||||
self.assertEqual(Token.objects.count(), 1)
|
||||
token = Token.objects.first()
|
||||
self.assertEqual(token.user, self.user)
|
||||
|
||||
def test_should_not_generate_api_token_if_exists(self):
|
||||
Token.objects.get_or_create(user=self.user)
|
||||
self.assertEqual(Token.objects.count(), 1)
|
||||
|
||||
self.client.get(reverse('bookmarks:settings.api'))
|
||||
|
||||
self.assertEqual(Token.objects.count(), 1)
|
@@ -26,6 +26,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
form_data = {
|
||||
'theme': UserProfile.THEME_DARK,
|
||||
'bookmark_date_display': UserProfile.BOOKMARK_DATE_DISPLAY_HIDDEN,
|
||||
'bookmark_link_target': UserProfile.BOOKMARK_LINK_TARGET_SELF,
|
||||
}
|
||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
|
||||
@@ -34,3 +35,4 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.user.profile.theme, form_data['theme'])
|
||||
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'])
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
@@ -20,3 +21,20 @@ class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
response = self.client.get(reverse('bookmarks:settings.integrations'), follow=True)
|
||||
|
||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.integrations'))
|
||||
|
||||
def test_should_generate_api_token_if_not_exists(self):
|
||||
self.assertEqual(Token.objects.count(), 0)
|
||||
|
||||
self.client.get(reverse('bookmarks:settings.integrations'))
|
||||
|
||||
self.assertEqual(Token.objects.count(), 1)
|
||||
token = Token.objects.first()
|
||||
self.assertEqual(token.user, self.user)
|
||||
|
||||
def test_should_not_generate_api_token_if_exists(self):
|
||||
Token.objects.get_or_create(user=self.user)
|
||||
self.assertEqual(Token.objects.count(), 1)
|
||||
|
||||
self.client.get(reverse('bookmarks:settings.integrations'))
|
||||
|
||||
self.assertEqual(Token.objects.count(), 1)
|
||||
|
@@ -25,3 +25,9 @@ class TagTestCase(TestCase):
|
||||
def test_parse_tag_string_deduplicates_tag_names(self):
|
||||
self.assertEqual(len(parse_tag_string('book,book,Book,BOOK')), 1)
|
||||
|
||||
def test_parse_tag_string_handles_duplicate_separators(self):
|
||||
self.assertCountEqual(parse_tag_string('book,,movie,,,album'), ['album', 'book', 'movie'])
|
||||
|
||||
def test_parse_tag_string_replaces_whitespace_within_names(self):
|
||||
self.assertCountEqual(parse_tag_string('travel guide, book recommendations'),
|
||||
['travel-guide', 'book-recommendations'])
|
||||
|
@@ -23,7 +23,6 @@ urlpatterns = [
|
||||
path('settings', views.settings.general, name='settings.index'),
|
||||
path('settings/general', views.settings.general, name='settings.general'),
|
||||
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
|
||||
path('settings/api', views.settings.api, name='settings.api'),
|
||||
path('settings/import', views.settings.bookmark_import, name='settings.import'),
|
||||
path('settings/export', views.settings.bookmark_export, name='settings.export'),
|
||||
# API
|
||||
|
@@ -40,6 +40,7 @@ def get_bookmark_view_context(request, query_set, tags, base_url):
|
||||
paginator = Paginator(query_set, _default_page_size)
|
||||
bookmarks = paginator.get_page(page)
|
||||
return_url = generate_return_url(base_url, page, query_string)
|
||||
link_target = request.user.profile.bookmark_link_target
|
||||
|
||||
if request.GET.get('tag'):
|
||||
mod = request.GET.copy()
|
||||
@@ -51,7 +52,8 @@ def get_bookmark_view_context(request, query_set, tags, base_url):
|
||||
'tags': tags,
|
||||
'query': query_string if query_string else '',
|
||||
'empty': paginator.count == 0,
|
||||
'return_url': return_url
|
||||
'return_url': return_url,
|
||||
'link_target': link_target,
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +68,12 @@ def generate_return_url(base_url, page, query_string):
|
||||
return urllib.parse.quote_plus(return_url)
|
||||
|
||||
|
||||
def convert_tag_string(tag_string: str):
|
||||
# Tag strings coming from inputs are space-separated, however services.bookmarks functions expect comma-separated
|
||||
# strings
|
||||
return tag_string.replace(' ', ',')
|
||||
|
||||
|
||||
@login_required
|
||||
def new(request):
|
||||
initial_url = request.GET.get('url')
|
||||
@@ -76,7 +84,8 @@ def new(request):
|
||||
auto_close = form.data['auto_close']
|
||||
if form.is_valid():
|
||||
current_user = request.user
|
||||
create_bookmark(form.save(commit=False), form.data['tag_string'], current_user)
|
||||
tag_string = convert_tag_string(form.data['tag_string'])
|
||||
create_bookmark(form.save(commit=False), tag_string, current_user)
|
||||
if auto_close:
|
||||
return HttpResponseRedirect(reverse('bookmarks:close'))
|
||||
else:
|
||||
@@ -105,7 +114,8 @@ def edit(request, bookmark_id: int):
|
||||
form = BookmarkForm(request.POST, instance=bookmark)
|
||||
return_url = form.data['return_url']
|
||||
if form.is_valid():
|
||||
update_bookmark(form.save(commit=False), form.data['tag_string'], request.user)
|
||||
tag_string = convert_tag_string(form.data['tag_string'])
|
||||
update_bookmark(form.save(commit=False), tag_string, request.user)
|
||||
return HttpResponseRedirect(return_url)
|
||||
else:
|
||||
return_url = request.GET.get('return_url')
|
||||
@@ -164,10 +174,10 @@ def bulk_edit(request):
|
||||
if 'bulk_delete' in request.POST:
|
||||
delete_bookmarks(bookmark_ids, request.user)
|
||||
if 'bulk_tag' in request.POST:
|
||||
tag_string = request.POST['bulk_tag_string']
|
||||
tag_string = convert_tag_string(request.POST['bulk_tag_string'])
|
||||
tag_bookmarks(bookmark_ids, tag_string, request.user)
|
||||
if 'bulk_untag' in request.POST:
|
||||
tag_string = request.POST['bulk_tag_string']
|
||||
tag_string = convert_tag_string(request.POST['bulk_tag_string'])
|
||||
untag_bookmarks(bookmark_ids, tag_string, request.user)
|
||||
|
||||
return_url = request.GET.get('return_url')
|
||||
|
@@ -43,15 +43,9 @@ def general(request):
|
||||
@login_required
|
||||
def integrations(request):
|
||||
application_url = request.build_absolute_uri("/bookmarks/new")
|
||||
api_token = Token.objects.get_or_create(user=request.user)[0]
|
||||
return render(request, 'settings/integrations.html', {
|
||||
'application_url': application_url,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def api(request):
|
||||
api_token = Token.objects.get_or_create(user=request.user)[0]
|
||||
return render(request, 'settings/api.html', {
|
||||
'api_token': api_token.key
|
||||
})
|
||||
|
||||
|
@@ -8,4 +8,6 @@ services:
|
||||
- "${LD_HOST_PORT:-9090}:9090"
|
||||
volumes:
|
||||
- "${LD_HOST_DATA_DIR:-./data}:/etc/linkding/data"
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkding",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.5",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@@ -25,11 +25,14 @@ urlpatterns = [
|
||||
extra_context=dict(allow_registration=ALLOW_REGISTRATION)),
|
||||
name='login'),
|
||||
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
|
||||
path('change-password/', auth_views.PasswordChangeView.as_view(), name='change_password'),
|
||||
path('password-change-done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
|
||||
path('', include('bookmarks.urls')),
|
||||
]
|
||||
|
||||
if DEBUG:
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
|
||||
|
||||
if ALLOW_REGISTRATION:
|
||||
|
@@ -1 +1 @@
|
||||
1.8.1
|
||||
1.8.5
|
||||
|
Reference in New Issue
Block a user