mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-09-15 13:39:56 +02:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3eb8cfe45e | ||
![]() |
f5b07eebba | ||
![]() |
3ba8f7e30b | ||
![]() |
9a63c367a8 | ||
![]() |
edb71286e7 | ||
![]() |
1ffc3e0266 | ||
![]() |
66995cfab2 | ||
![]() |
68143de992 | ||
![]() |
b93a9fadb6 | ||
![]() |
77fea02f77 | ||
![]() |
fcc0b6f591 | ||
![]() |
e1c9a7add6 | ||
![]() |
82b4268a26 | ||
![]() |
5287eb3f8b | ||
![]() |
d298260122 | ||
![]() |
12e5810aee | ||
![]() |
1dabd0266b | ||
![]() |
7390fc3f4f | ||
![]() |
5e003ede92 | ||
![]() |
984eef92e2 | ||
![]() |
eae6ca6e07 | ||
![]() |
a6bfaa7c78 |
@@ -8,6 +8,7 @@
|
|||||||
/static
|
/static
|
||||||
/build
|
/build
|
||||||
/out
|
/out
|
||||||
|
/.git
|
||||||
|
|
||||||
/.dockerignore
|
/.dockerignore
|
||||||
/.gitignore
|
/.gitignore
|
||||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.8.5 (13/12/2021)
|
||||||
|
- [**bug**] Ensure tag names do not contain spaces [#182](https://github.com/sissbruecker/linkding/issues/182)
|
||||||
|
- [**bug**] Consider not copying whole GIT repository to Docker image [#174](https://github.com/sissbruecker/linkding/issues/174)
|
||||||
|
- [**enhancement**] Make bookmarks count column in admin sortable [#183](https://github.com/sissbruecker/linkding/pull/183)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
## v1.8.2 (02/10/2021)
|
||||||
- [**bug**] Fix jumping search box [#163](https://github.com/sissbruecker/linkding/pull/163)
|
- [**bug**] Fix jumping search box [#163](https://github.com/sissbruecker/linkding/pull/163)
|
||||||
---
|
---
|
||||||
|
@@ -169,3 +169,4 @@ The frontend is now available under http://localhost:8000
|
|||||||
## Community
|
## Community
|
||||||
|
|
||||||
- [linkding-extension](https://github.com/jeroenpardon/linkding-extension) Chromium compatible extension that wraps the linkding bookmarklet. Tested with Chrome, Edge, Brave. By [jeroenpardon](https://github.com/jeroenpardon)
|
- [linkding-extension](https://github.com/jeroenpardon/linkding-extension) Chromium compatible extension that wraps the linkding bookmarklet. Tested with Chrome, Edge, Brave. By [jeroenpardon](https://github.com/jeroenpardon)
|
||||||
|
- [linkding-injector](https://github.com/Fivefold/linkding-injector) Injects search results from linkding into the sidebar of search pages like google and duckduckgo. Tested with Firefox and Chrome. By [Fivefold](https://github.com/Fivefold)
|
||||||
|
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 1.8.x | :white_check_mark: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
To report a vulnerability, please send a mail to: 588ex5zl8@mozmail.com
|
||||||
|
|
||||||
|
I'll try to get back to you as soon as possible.
|
@@ -59,6 +59,8 @@ class AdminTag(admin.ModelAdmin):
|
|||||||
def bookmarks_count(self, obj):
|
def bookmarks_count(self, obj):
|
||||||
return obj.bookmarks_count
|
return obj.bookmarks_count
|
||||||
|
|
||||||
|
bookmarks_count.admin_order_field = 'bookmarks_count'
|
||||||
|
|
||||||
def delete_unused_tags(self, request, queryset: QuerySet):
|
def delete_unused_tags(self, request, queryset: QuerySet):
|
||||||
unused_tags = queryset.filter(bookmark__isnull=True)
|
unused_tags = queryset.filter(bookmark__isnull=True)
|
||||||
unused_tags_count = unused_tags.count()
|
unused_tags_count = unused_tags.count()
|
||||||
|
@@ -41,14 +41,14 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
|||||||
bookmark.url = validated_data['url']
|
bookmark.url = validated_data['url']
|
||||||
bookmark.title = validated_data['title']
|
bookmark.title = validated_data['title']
|
||||||
bookmark.description = validated_data['description']
|
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'])
|
return create_bookmark(bookmark, tag_string, self.context['user'])
|
||||||
|
|
||||||
def update(self, instance: Bookmark, validated_data):
|
def update(self, instance: Bookmark, validated_data):
|
||||||
instance.url = validated_data['url']
|
instance.url = validated_data['url']
|
||||||
instance.title = validated_data['title']
|
instance.title = validated_data['title']
|
||||||
instance.description = validated_data['description']
|
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'])
|
return update_bookmark(instance, tag_string, self.context['user'])
|
||||||
|
|
||||||
|
|
||||||
|
@@ -20,11 +20,19 @@ class Tag(models.Model):
|
|||||||
return self.name
|
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 = ','):
|
def parse_tag_string(tag_string: str, delimiter: str = ','):
|
||||||
if not tag_string:
|
if not tag_string:
|
||||||
return []
|
return []
|
||||||
names = tag_string.strip().split(delimiter)
|
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 = unique(names, str.lower)
|
||||||
names.sort(key=str.lower)
|
names.sort(key=str.lower)
|
||||||
|
|
||||||
@@ -91,12 +99,10 @@ class BookmarkForm(forms.ModelForm):
|
|||||||
widget=forms.Textarea())
|
widget=forms.Textarea())
|
||||||
# Hidden field that determines whether to close window/tab after saving the bookmark
|
# Hidden field that determines whether to close window/tab after saving the bookmark
|
||||||
auto_close = forms.CharField(required=False)
|
auto_close = forms.CharField(required=False)
|
||||||
# Hidden field that determines where to redirect after saving the form
|
|
||||||
return_url = forms.CharField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bookmark
|
model = Bookmark
|
||||||
fields = ['url', 'tag_string', 'title', 'description', 'auto_close', 'return_url']
|
fields = ['url', 'tag_string', 'title', 'description', 'auto_close']
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(models.Model):
|
class UserProfile(models.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):
|
def tag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_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)
|
tags = get_or_create_tags(tag_names, current_user)
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
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):
|
def untag_bookmarks(bookmark_ids: [Union[int, str]], tag_string: str, current_user: User):
|
||||||
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
sanitized_bookmark_ids = _sanitize_id_list(bookmark_ids)
|
||||||
bookmarks = Bookmark.objects.filter(owner=current_user, id__in=sanitized_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)
|
tags = get_or_create_tags(tag_names, current_user)
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
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):
|
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)
|
tags = get_or_create_tags(tag_names, user)
|
||||||
bookmark.tags.set(tags)
|
bookmark.tags.set(tags)
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<div class="content-area-header">
|
<div class="content-area-header">
|
||||||
<h2>Edit bookmark</h2>
|
<h2>Edit bookmark</h2>
|
||||||
</div>
|
</div>
|
||||||
<form action="{% url 'bookmarks:edit' bookmark_id %}" method="post" class="col-6 col-md-12" novalidate>
|
<form action="{% url 'bookmarks:edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post" class="col-6 col-md-12" novalidate>
|
||||||
{% bookmark_form form return_url bookmark_id %}
|
{% bookmark_form form return_url bookmark_id %}
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -4,7 +4,6 @@
|
|||||||
<div class="bookmarks-form">
|
<div class="bookmarks-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.auto_close|attr:"type:hidden" }}
|
{{ form.auto_close|attr:"type:hidden" }}
|
||||||
{{ form.return_url|attr:"type:hidden" }}
|
|
||||||
<div class="form-group {% if form.url.errors %}has-error{% endif %}">
|
<div class="form-group {% if form.url.errors %}has-error{% endif %}">
|
||||||
<label for="{{ form.url.id_for_label }}" class="form-label">URL</label>
|
<label for="{{ form.url.id_for_label }}" class="form-label">URL</label>
|
||||||
{{ form.url|add_class:"form-input"|attr:"autofocus"|attr:"placeholder: " }}
|
{{ form.url|add_class:"form-input"|attr:"autofocus"|attr:"placeholder: " }}
|
||||||
|
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 #}
|
{# Profile section #}
|
||||||
<section class="content-area">
|
<section class="content-area">
|
||||||
<h2>Profile</h2>
|
<h2>Profile</h2>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'change_password' %}">Change password</a>
|
||||||
|
</p>
|
||||||
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
|
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
{% include 'settings/nav.html' %}
|
{% include 'settings/nav.html' %}
|
||||||
|
|
||||||
{# Integrations section #}
|
|
||||||
<section class="content-area">
|
<section class="content-area">
|
||||||
<h2>Browser Extension</h2>
|
<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>
|
<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>
|
class="btn btn-primary">📎 Add bookmark</a>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
{% url 'bookmarks:settings.index' as index_url %}
|
{% url 'bookmarks:settings.index' as index_url %}
|
||||||
{% url 'bookmarks:settings.general' as general_url %}
|
{% url 'bookmarks:settings.general' as general_url %}
|
||||||
{% url 'bookmarks:settings.integrations' as integrations_url %}
|
{% url 'bookmarks:settings.integrations' as integrations_url %}
|
||||||
{% url 'bookmarks:settings.api' as api_url %}
|
|
||||||
|
|
||||||
<ul class="tab tab-block">
|
<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 %}">
|
<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 %}">
|
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
|
||||||
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
|
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
|
||||||
</li>
|
</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.">
|
<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">
|
<a href="{% url 'admin:index' %}" target="_blank">
|
||||||
<span>Admin</span>
|
<span>Admin</span>
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@@ -33,3 +34,22 @@ class BookmarkArchiveViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
self.assertRedirects(response, reverse('bookmarks:close'))
|
||||||
|
|
||||||
|
def test_can_only_archive_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
bookmark = self.setup_bookmark(user=other_user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('bookmarks:archive', args=[bookmark.id]))
|
||||||
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertFalse(bookmark.is_archived)
|
||||||
|
|
||||||
|
def test_should_not_redirect_to_external_url(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse('bookmarks:archive', args=[bookmark.id]) + '?return_url=https://example.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
from django.forms import model_to_dict
|
from django.forms import model_to_dict
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -32,6 +33,21 @@ class BookmarkBulkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||||
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||||
|
|
||||||
|
def test_can_only_bulk_archive_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
bookmark1 = self.setup_bookmark(user=other_user)
|
||||||
|
bookmark2 = self.setup_bookmark(user=other_user)
|
||||||
|
bookmark3 = self.setup_bookmark(user=other_user)
|
||||||
|
|
||||||
|
self.client.post(reverse('bookmarks:bulk_edit'), {
|
||||||
|
'bulk_archive': [''],
|
||||||
|
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertFalse(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||||
|
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||||
|
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||||
|
|
||||||
def test_bulk_unarchive(self):
|
def test_bulk_unarchive(self):
|
||||||
bookmark1 = self.setup_bookmark(is_archived=True)
|
bookmark1 = self.setup_bookmark(is_archived=True)
|
||||||
bookmark2 = self.setup_bookmark(is_archived=True)
|
bookmark2 = self.setup_bookmark(is_archived=True)
|
||||||
@@ -46,6 +62,21 @@ class BookmarkBulkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
self.assertFalse(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||||
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
self.assertFalse(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||||
|
|
||||||
|
def test_can_only_bulk_unarchive_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
bookmark1 = self.setup_bookmark(is_archived=True, user=other_user)
|
||||||
|
bookmark2 = self.setup_bookmark(is_archived=True, user=other_user)
|
||||||
|
bookmark3 = self.setup_bookmark(is_archived=True, user=other_user)
|
||||||
|
|
||||||
|
self.client.post(reverse('bookmarks:bulk_edit'), {
|
||||||
|
'bulk_unarchive': [''],
|
||||||
|
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertTrue(Bookmark.objects.get(id=bookmark1.id).is_archived)
|
||||||
|
self.assertTrue(Bookmark.objects.get(id=bookmark2.id).is_archived)
|
||||||
|
self.assertTrue(Bookmark.objects.get(id=bookmark3.id).is_archived)
|
||||||
|
|
||||||
def test_bulk_delete(self):
|
def test_bulk_delete(self):
|
||||||
bookmark1 = self.setup_bookmark()
|
bookmark1 = self.setup_bookmark()
|
||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
@@ -57,8 +88,23 @@ class BookmarkBulkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
})
|
})
|
||||||
|
|
||||||
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
|
self.assertIsNone(Bookmark.objects.filter(id=bookmark1.id).first())
|
||||||
self.assertFalse(Bookmark.objects.filter(id=bookmark2.id).first())
|
self.assertIsNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||||
self.assertFalse(Bookmark.objects.filter(id=bookmark3.id).first())
|
self.assertIsNone(Bookmark.objects.filter(id=bookmark3.id).first())
|
||||||
|
|
||||||
|
def test_can_only_bulk_delete_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
bookmark1 = self.setup_bookmark(user=other_user)
|
||||||
|
bookmark2 = self.setup_bookmark(user=other_user)
|
||||||
|
bookmark3 = self.setup_bookmark(user=other_user)
|
||||||
|
|
||||||
|
self.client.post(reverse('bookmarks:bulk_edit'), {
|
||||||
|
'bulk_delete': [''],
|
||||||
|
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark1.id).first())
|
||||||
|
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark2.id).first())
|
||||||
|
self.assertIsNotNone(Bookmark.objects.filter(id=bookmark3.id).first())
|
||||||
|
|
||||||
def test_bulk_tag(self):
|
def test_bulk_tag(self):
|
||||||
bookmark1 = self.setup_bookmark()
|
bookmark1 = self.setup_bookmark()
|
||||||
@@ -81,6 +127,28 @@ class BookmarkBulkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
||||||
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||||
|
|
||||||
|
def test_can_only_bulk_tag_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
bookmark1 = self.setup_bookmark(user=other_user)
|
||||||
|
bookmark2 = self.setup_bookmark(user=other_user)
|
||||||
|
bookmark3 = self.setup_bookmark(user=other_user)
|
||||||
|
tag1 = self.setup_tag()
|
||||||
|
tag2 = self.setup_tag()
|
||||||
|
|
||||||
|
self.client.post(reverse('bookmarks:bulk_edit'), {
|
||||||
|
'bulk_tag': [''],
|
||||||
|
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
||||||
|
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||||
|
})
|
||||||
|
|
||||||
|
bookmark1.refresh_from_db()
|
||||||
|
bookmark2.refresh_from_db()
|
||||||
|
bookmark3.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertCountEqual(bookmark1.tags.all(), [])
|
||||||
|
self.assertCountEqual(bookmark2.tags.all(), [])
|
||||||
|
self.assertCountEqual(bookmark3.tags.all(), [])
|
||||||
|
|
||||||
def test_bulk_untag(self):
|
def test_bulk_untag(self):
|
||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
@@ -102,6 +170,28 @@ class BookmarkBulkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertCountEqual(bookmark2.tags.all(), [])
|
self.assertCountEqual(bookmark2.tags.all(), [])
|
||||||
self.assertCountEqual(bookmark3.tags.all(), [])
|
self.assertCountEqual(bookmark3.tags.all(), [])
|
||||||
|
|
||||||
|
def test_can_only_bulk_untag_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
tag1 = self.setup_tag()
|
||||||
|
tag2 = self.setup_tag()
|
||||||
|
bookmark1 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
||||||
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
||||||
|
bookmark3 = self.setup_bookmark(tags=[tag1, tag2], user=other_user)
|
||||||
|
|
||||||
|
self.client.post(reverse('bookmarks:bulk_edit'), {
|
||||||
|
'bulk_untag': [''],
|
||||||
|
'bulk_tag_string': [f'{tag1.name} {tag2.name}'],
|
||||||
|
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||||
|
})
|
||||||
|
|
||||||
|
bookmark1.refresh_from_db()
|
||||||
|
bookmark2.refresh_from_db()
|
||||||
|
bookmark3.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2])
|
||||||
|
self.assertCountEqual(bookmark2.tags.all(), [tag1, tag2])
|
||||||
|
self.assertCountEqual(bookmark3.tags.all(), [tag1, tag2])
|
||||||
|
|
||||||
def test_bulk_edit_handles_empty_bookmark_id(self):
|
def test_bulk_edit_handles_empty_bookmark_id(self):
|
||||||
bookmark1 = self.setup_bookmark()
|
bookmark1 = self.setup_bookmark()
|
||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
@@ -130,3 +220,29 @@ class BookmarkBulkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
})
|
})
|
||||||
|
|
||||||
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
|
self.assertBookmarksAreUnmodified([bookmark1, bookmark2, bookmark3])
|
||||||
|
|
||||||
|
def test_bulk_edit_should_redirect_to_return_url(self):
|
||||||
|
bookmark1 = self.setup_bookmark()
|
||||||
|
bookmark2 = self.setup_bookmark()
|
||||||
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
|
url = reverse('bookmarks:bulk_edit') + '?return_url=' + reverse('bookmarks:settings.index')
|
||||||
|
response = self.client.post(url, {
|
||||||
|
'bulk_archive': [''],
|
||||||
|
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('bookmarks:settings.index'))
|
||||||
|
|
||||||
|
def test_bulk_edit_should_not_redirect_to_external_url(self):
|
||||||
|
bookmark1 = self.setup_bookmark()
|
||||||
|
bookmark2 = self.setup_bookmark()
|
||||||
|
bookmark3 = self.setup_bookmark()
|
||||||
|
|
||||||
|
url = reverse('bookmarks:bulk_edit') + '?return_url=https://example.com'
|
||||||
|
response = self.client.post(url, {
|
||||||
|
'bulk_archive': [''],
|
||||||
|
'bookmark_id': [str(bookmark1.id), str(bookmark2.id), str(bookmark3.id)],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@@ -19,7 +20,6 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
'tag_string': 'editedtag1 editedtag2',
|
'tag_string': 'editedtag1 editedtag2',
|
||||||
'title': 'edited title',
|
'title': 'edited title',
|
||||||
'description': 'edited description',
|
'description': 'edited description',
|
||||||
'return_url': reverse('bookmarks:index'),
|
|
||||||
}
|
}
|
||||||
return {**form_data, **overrides}
|
return {**form_data, **overrides}
|
||||||
|
|
||||||
@@ -39,17 +39,6 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertEqual(bookmark.tags.all()[0].name, 'editedtag1')
|
self.assertEqual(bookmark.tags.all()[0].name, 'editedtag1')
|
||||||
self.assertEqual(bookmark.tags.all()[1].name, 'editedtag2')
|
self.assertEqual(bookmark.tags.all()[1].name, 'editedtag2')
|
||||||
|
|
||||||
def test_should_use_bookmark_index_as_default_return_url(self):
|
|
||||||
bookmark = self.setup_bookmark()
|
|
||||||
|
|
||||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
|
||||||
html = response.content.decode()
|
|
||||||
|
|
||||||
self.assertInHTML(
|
|
||||||
'<input type="hidden" name="return_url" value="{0}" '
|
|
||||||
'id="id_return_url">'.format(reverse('bookmarks:index')),
|
|
||||||
html)
|
|
||||||
|
|
||||||
def test_should_prefill_bookmark_form_fields(self):
|
def test_should_prefill_bookmark_form_fields(self):
|
||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = self.setup_tag()
|
tag2 = self.setup_tag()
|
||||||
@@ -80,18 +69,38 @@ class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
'</textarea>'.format(bookmark.description),
|
'</textarea>'.format(bookmark.description),
|
||||||
html)
|
html)
|
||||||
|
|
||||||
def test_should_prefill_return_url_from_url_parameter(self):
|
|
||||||
bookmark = self.setup_bookmark()
|
|
||||||
|
|
||||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]) + '?return_url=/test-return-url')
|
|
||||||
html = response.content.decode()
|
|
||||||
|
|
||||||
self.assertInHTML('<input type="hidden" name="return_url" value="/test-return-url" id="id_return_url">', html)
|
|
||||||
|
|
||||||
def test_should_redirect_to_return_url(self):
|
def test_should_redirect_to_return_url(self):
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
form_data = self.create_form_data({'return_url': reverse('bookmarks:close')})
|
form_data = self.create_form_data()
|
||||||
|
|
||||||
|
url = reverse('bookmarks:edit', args=[bookmark.id]) + '?return_url=' + reverse('bookmarks:close')
|
||||||
|
response = self.client.post(url, form_data)
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('bookmarks:close'))
|
||||||
|
|
||||||
|
def test_should_redirect_to_bookmark_index_by_default(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
form_data = self.create_form_data()
|
||||||
|
|
||||||
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||||
|
|
||||||
self.assertRedirects(response, form_data['return_url'])
|
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||||
|
|
||||||
|
def test_should_not_redirect_to_external_url(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
form_data = self.create_form_data({'return_url': 'https://example.com'})
|
||||||
|
|
||||||
|
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||||
|
|
||||||
|
def test_can_only_edit_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
bookmark = self.setup_bookmark(user=other_user)
|
||||||
|
form_data = self.create_form_data({'id': bookmark.id})
|
||||||
|
|
||||||
|
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||||
|
bookmark.refresh_from_db()
|
||||||
|
self.assertNotEqual(bookmark.url, form_data['url'])
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
@@ -73,6 +73,13 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
|
|
||||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||||
|
|
||||||
|
def test_should_not_redirect_to_external_url(self):
|
||||||
|
form_data = self.create_form_data()
|
||||||
|
|
||||||
|
response = self.client.post(reverse('bookmarks:new') + '?return_url=https://example.com', form_data)
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||||
|
|
||||||
def test_auto_close_should_redirect_to_close_view(self):
|
def test_auto_close_should_redirect_to_close_view(self):
|
||||||
form_data = self.create_form_data({'auto_close': 'true'})
|
form_data = self.create_form_data({'auto_close': 'true'})
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@@ -33,3 +34,21 @@ class BookmarkRemoveViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
self.assertRedirects(response, reverse('bookmarks:close'))
|
||||||
|
|
||||||
|
def test_should_not_redirect_to_external_url(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse('bookmarks:remove', args=[bookmark.id]) + '?return_url=https://example.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||||
|
|
||||||
|
def test_can_only_edit_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
bookmark = self.setup_bookmark(user=other_user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('bookmarks:remove', args=[bookmark.id]))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertTrue(Bookmark.objects.filter(id=bookmark.id).exists())
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@@ -33,3 +34,22 @@ class BookmarkUnarchiveViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
self.assertRedirects(response, reverse('bookmarks:close'))
|
||||||
|
|
||||||
|
def test_should_not_redirect_to_external_url(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse('bookmarks:unarchive', args=[bookmark.id]) + '?return_url=https://example.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRedirects(response, reverse('bookmarks:archived'))
|
||||||
|
|
||||||
|
def test_can_only_archive_own_bookmarks(self):
|
||||||
|
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||||
|
bookmark = self.setup_bookmark(is_archived=True, user=other_user)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('bookmarks:unarchive', args=[bookmark.id]))
|
||||||
|
bookmark.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertTrue(bookmark.is_archived)
|
||||||
|
@@ -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'][0]).count(), 1)
|
||||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
|
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
|
||||||
|
|
||||||
|
def test_create_bookmark_replaces_whitespace_in_tag_names(self):
|
||||||
|
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):
|
def test_create_bookmark_minimal_payload(self):
|
||||||
data = {'url': 'https://example.com/'}
|
data = {'url': 'https://example.com/'}
|
||||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||||
|
@@ -21,7 +21,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
def test_create_should_create_web_archive_snapshot(self):
|
def test_create_should_create_web_archive_snapshot(self):
|
||||||
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||||
bookmark_data = Bookmark(url='https://example.com')
|
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)
|
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:
|
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
bookmark.url = 'https://example.com/updated'
|
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)
|
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:
|
with patch.object(tasks, 'create_web_archive_snapshot') as mock_create_web_archive_snapshot:
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
bookmark.title = 'updated title'
|
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()
|
mock_create_web_archive_snapshot.assert_not_called()
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = 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())
|
self.get_or_create_test_user())
|
||||||
|
|
||||||
bookmark1.refresh_from_db()
|
bookmark1.refresh_from_db()
|
||||||
@@ -232,7 +232,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark()
|
bookmark2 = self.setup_bookmark()
|
||||||
bookmark3 = 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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -257,7 +257,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = 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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -275,7 +275,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = 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())
|
self.get_or_create_test_user())
|
||||||
|
|
||||||
bookmark1.refresh_from_db()
|
bookmark1.refresh_from_db()
|
||||||
@@ -293,7 +293,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag1 = self.setup_tag()
|
tag1 = self.setup_tag()
|
||||||
tag2 = 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.get_or_create_test_user())
|
||||||
|
|
||||||
self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2])
|
self.assertCountEqual(bookmark1.tags.all(), [tag1, tag2])
|
||||||
@@ -307,7 +307,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
bookmark3 = 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())
|
self.get_or_create_test_user())
|
||||||
|
|
||||||
bookmark1.refresh_from_db()
|
bookmark1.refresh_from_db()
|
||||||
@@ -325,7 +325,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
bookmark3 = 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()
|
bookmark1.refresh_from_db()
|
||||||
bookmark2.refresh_from_db()
|
bookmark2.refresh_from_db()
|
||||||
@@ -343,7 +343,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
inaccessible_bookmark = self.setup_bookmark(user=other_user, 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())
|
self.get_or_create_test_user())
|
||||||
|
|
||||||
bookmark1.refresh_from_db()
|
bookmark1.refresh_from_db()
|
||||||
@@ -361,7 +361,7 @@ class BookmarkServiceTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
bookmark2 = self.setup_bookmark(tags=[tag1, tag2])
|
||||||
bookmark3 = 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.get_or_create_test_user())
|
||||||
|
|
||||||
self.assertCountEqual(bookmark1.tags.all(), [])
|
self.assertCountEqual(bookmark1.tags.all(), [])
|
||||||
|
@@ -2,6 +2,7 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from bookmarks.models import Tag
|
||||||
from bookmarks.services import tasks
|
from bookmarks.services import tasks
|
||||||
from bookmarks.services.importer import import_netscape_html
|
from bookmarks.services.importer import import_netscape_html
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging
|
from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging
|
||||||
@@ -20,6 +21,18 @@ class ImporterTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
</DL><p>
|
</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
|
@disable_logging
|
||||||
def test_validate_empty_or_missing_bookmark_url(self):
|
def test_validate_empty_or_missing_bookmark_url(self):
|
||||||
test_html = self.create_import_html(f'''
|
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)
|
|
@@ -1,5 +1,6 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||||
|
|
||||||
@@ -20,3 +21,20 @@ class SettingsIntegrationsViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
response = self.client.get(reverse('bookmarks:settings.integrations'), follow=True)
|
response = self.client.get(reverse('bookmarks:settings.integrations'), follow=True)
|
||||||
|
|
||||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.integrations'))
|
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):
|
def test_parse_tag_string_deduplicates_tag_names(self):
|
||||||
self.assertEqual(len(parse_tag_string('book,book,Book,BOOK')), 1)
|
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', views.settings.general, name='settings.index'),
|
||||||
path('settings/general', views.settings.general, name='settings.general'),
|
path('settings/general', views.settings.general, name='settings.general'),
|
||||||
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
|
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/import', views.settings.bookmark_import, name='settings.import'),
|
||||||
path('settings/export', views.settings.bookmark_export, name='settings.export'),
|
path('settings/export', views.settings.bookmark_export, name='settings.export'),
|
||||||
# API
|
# API
|
||||||
|
@@ -95,3 +95,10 @@ def parse_timestamp(value: str):
|
|||||||
|
|
||||||
# Timestamp is out of range
|
# Timestamp is out of range
|
||||||
raise ValueError(f'{value} exceeds maximum value for a timestamp')
|
raise ValueError(f'{value} exceeds maximum value for a timestamp')
|
||||||
|
|
||||||
|
|
||||||
|
def get_safe_return_url(return_url: str, fallback_url: str):
|
||||||
|
# Use fallback if URL is none or URL is not on same domain
|
||||||
|
if not return_url or not return_url.startswith('/'):
|
||||||
|
return fallback_url
|
||||||
|
return return_url
|
||||||
|
@@ -2,7 +2,7 @@ import urllib.parse
|
|||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect, Http404
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ from bookmarks import queries
|
|||||||
from bookmarks.models import Bookmark, BookmarkForm, build_tag_string
|
from bookmarks.models import Bookmark, BookmarkForm, build_tag_string
|
||||||
from bookmarks.services.bookmarks import create_bookmark, update_bookmark, archive_bookmark, archive_bookmarks, \
|
from bookmarks.services.bookmarks import create_bookmark, update_bookmark, archive_bookmark, archive_bookmarks, \
|
||||||
unarchive_bookmark, unarchive_bookmarks, delete_bookmarks, tag_bookmarks, untag_bookmarks
|
unarchive_bookmark, unarchive_bookmarks, delete_bookmarks, tag_bookmarks, untag_bookmarks
|
||||||
|
from bookmarks.utils import get_safe_return_url
|
||||||
|
|
||||||
_default_page_size = 30
|
_default_page_size = 30
|
||||||
|
|
||||||
@@ -68,6 +69,12 @@ def generate_return_url(base_url, page, query_string):
|
|||||||
return urllib.parse.quote_plus(return_url)
|
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
|
@login_required
|
||||||
def new(request):
|
def new(request):
|
||||||
initial_url = request.GET.get('url')
|
initial_url = request.GET.get('url')
|
||||||
@@ -78,7 +85,8 @@ def new(request):
|
|||||||
auto_close = form.data['auto_close']
|
auto_close = form.data['auto_close']
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
current_user = request.user
|
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:
|
if auto_close:
|
||||||
return HttpResponseRedirect(reverse('bookmarks:close'))
|
return HttpResponseRedirect(reverse('bookmarks:close'))
|
||||||
else:
|
else:
|
||||||
@@ -101,22 +109,22 @@ def new(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def edit(request, bookmark_id: int):
|
def edit(request, bookmark_id: int):
|
||||||
bookmark = Bookmark.objects.get(pk=bookmark_id)
|
try:
|
||||||
|
bookmark = Bookmark.objects.get(pk=bookmark_id, owner=request.user)
|
||||||
|
except Bookmark.DoesNotExist:
|
||||||
|
raise Http404('Bookmark does not exist')
|
||||||
|
return_url = get_safe_return_url(request.GET.get('return_url'), reverse('bookmarks:index'))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = BookmarkForm(request.POST, instance=bookmark)
|
form = BookmarkForm(request.POST, instance=bookmark)
|
||||||
return_url = form.data['return_url']
|
|
||||||
if form.is_valid():
|
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)
|
return HttpResponseRedirect(return_url)
|
||||||
else:
|
else:
|
||||||
return_url = request.GET.get('return_url')
|
|
||||||
form = BookmarkForm(instance=bookmark)
|
form = BookmarkForm(instance=bookmark)
|
||||||
|
|
||||||
return_url = return_url if return_url else reverse('bookmarks:index')
|
|
||||||
|
|
||||||
form.initial['tag_string'] = build_tag_string(bookmark.tag_names, ' ')
|
form.initial['tag_string'] = build_tag_string(bookmark.tag_names, ' ')
|
||||||
form.initial['return_url'] = return_url
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'form': form,
|
'form': form,
|
||||||
@@ -129,28 +137,37 @@ def edit(request, bookmark_id: int):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def remove(request, bookmark_id: int):
|
def remove(request, bookmark_id: int):
|
||||||
bookmark = Bookmark.objects.get(pk=bookmark_id)
|
try:
|
||||||
|
bookmark = Bookmark.objects.get(pk=bookmark_id, owner=request.user)
|
||||||
|
except Bookmark.DoesNotExist:
|
||||||
|
raise Http404('Bookmark does not exist')
|
||||||
|
|
||||||
bookmark.delete()
|
bookmark.delete()
|
||||||
return_url = request.GET.get('return_url')
|
return_url = get_safe_return_url(request.GET.get('return_url'), reverse('bookmarks:index'))
|
||||||
return_url = return_url if return_url else reverse('bookmarks:index')
|
|
||||||
return HttpResponseRedirect(return_url)
|
return HttpResponseRedirect(return_url)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def archive(request, bookmark_id: int):
|
def archive(request, bookmark_id: int):
|
||||||
bookmark = Bookmark.objects.get(pk=bookmark_id)
|
try:
|
||||||
|
bookmark = Bookmark.objects.get(pk=bookmark_id, owner=request.user)
|
||||||
|
except Bookmark.DoesNotExist:
|
||||||
|
raise Http404('Bookmark does not exist')
|
||||||
|
|
||||||
archive_bookmark(bookmark)
|
archive_bookmark(bookmark)
|
||||||
return_url = request.GET.get('return_url')
|
return_url = get_safe_return_url(request.GET.get('return_url'), reverse('bookmarks:index'))
|
||||||
return_url = return_url if return_url else reverse('bookmarks:index')
|
|
||||||
return HttpResponseRedirect(return_url)
|
return HttpResponseRedirect(return_url)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def unarchive(request, bookmark_id: int):
|
def unarchive(request, bookmark_id: int):
|
||||||
bookmark = Bookmark.objects.get(pk=bookmark_id)
|
try:
|
||||||
|
bookmark = Bookmark.objects.get(pk=bookmark_id, owner=request.user)
|
||||||
|
except Bookmark.DoesNotExist:
|
||||||
|
raise Http404('Bookmark does not exist')
|
||||||
|
|
||||||
unarchive_bookmark(bookmark)
|
unarchive_bookmark(bookmark)
|
||||||
return_url = request.GET.get('return_url')
|
return_url = get_safe_return_url(request.GET.get('return_url'), reverse('bookmarks:archived'))
|
||||||
return_url = return_url if return_url else reverse('bookmarks:archived')
|
|
||||||
return HttpResponseRedirect(return_url)
|
return HttpResponseRedirect(return_url)
|
||||||
|
|
||||||
|
|
||||||
@@ -166,14 +183,13 @@ def bulk_edit(request):
|
|||||||
if 'bulk_delete' in request.POST:
|
if 'bulk_delete' in request.POST:
|
||||||
delete_bookmarks(bookmark_ids, request.user)
|
delete_bookmarks(bookmark_ids, request.user)
|
||||||
if 'bulk_tag' in request.POST:
|
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)
|
tag_bookmarks(bookmark_ids, tag_string, request.user)
|
||||||
if 'bulk_untag' in request.POST:
|
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)
|
untag_bookmarks(bookmark_ids, tag_string, request.user)
|
||||||
|
|
||||||
return_url = request.GET.get('return_url')
|
return_url = get_safe_return_url(request.GET.get('return_url'), reverse('bookmarks:index'))
|
||||||
return_url = return_url if return_url else reverse('bookmarks:index')
|
|
||||||
return HttpResponseRedirect(return_url)
|
return HttpResponseRedirect(return_url)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -43,15 +43,9 @@ def general(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def integrations(request):
|
def integrations(request):
|
||||||
application_url = request.build_absolute_uri("/bookmarks/new")
|
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', {
|
return render(request, 'settings/integrations.html', {
|
||||||
'application_url': application_url,
|
'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
|
'api_token': api_token.key
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -8,4 +8,6 @@ services:
|
|||||||
- "${LD_HOST_PORT:-9090}:9090"
|
- "${LD_HOST_PORT:-9090}:9090"
|
||||||
volumes:
|
volumes:
|
||||||
- "${LD_HOST_DATA_DIR:-./data}:/etc/linkding/data"
|
- "${LD_HOST_DATA_DIR:-./data}:/etc/linkding/data"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
@@ -20,6 +20,22 @@ Now when you are browsing the web and you want to save the current page as a boo
|
|||||||
|
|
||||||
For more info see here: https://paul.kinlan.me/use-bookmarklets-on-chrome-on-android/
|
For more info see here: https://paul.kinlan.me/use-bookmarklets-on-chrome-on-android/
|
||||||
|
|
||||||
|
## Using HTTP Shortcuts app on Android
|
||||||
|
|
||||||
|
**Note** This allows you to share URL from any app to bookmark it to linkding
|
||||||
|
|
||||||
|
- Install HTTP Shortcuts from [Play Store](https://play.google.com/store/apps/details?id=ch.rmy.android.http_shortcuts) or [F-Droid](https://f-droid.org/en/packages/ch.rmy.android.http_shortcuts/).
|
||||||
|
|
||||||
|
- Download [linkding_shortcut.json](/docs/linkding_shortcut.json) from this repository.
|
||||||
|
|
||||||
|
- Open HTTP Shortcuts, tap the 3-dot-button at the top-right corner, tap `Import/Export`, then tap `Import from file`.
|
||||||
|
|
||||||
|
- Select the json file you downloaded earlier, go back, tap the 3-dot-button again, then tap `Variables`.
|
||||||
|
|
||||||
|
- Edit the `values` of `linkding_instance`, `linkding_tag` and `linkding_api_token`.
|
||||||
|
|
||||||
|
Try using share button on an app, a new item `Send to...` should appear on the share sheet. You can also manually share by tapping the shortcut inside the HTTP Shortcuts app itself.
|
||||||
|
|
||||||
## Create a share action on iOS for adding bookmarks to linkding
|
## Create a share action on iOS for adding bookmarks to linkding
|
||||||
|
|
||||||
This how-to explains how to make use of the app shortcuts iOS app to create a share action that can be used in Safari for adding bookmarks to your linkding instance.
|
This how-to explains how to make use of the app shortcuts iOS app to create a share action that can be used in Safari for adding bookmarks to your linkding instance.
|
||||||
|
59
docs/linkding_shortcut.json
Normal file
59
docs/linkding_shortcut.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": "8f4299d4-4c30-4a8e-a3f9-c90694011713",
|
||||||
|
"name": "Shortcuts",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"bodyContent": "{ \"url\": \"{{b2953f61-b302-4c79-b90d-39858a06d9a6}}\", \"tag_names\": [ \"{{c360f61f-ce17-47b4-bea3-1d8c3913ca52}}\" ] }",
|
||||||
|
"contentType": "application/json",
|
||||||
|
"description": "Bookmark to linkding",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"id": "d235f7b4-fce2-41f4-a00f-72d5fde9e4b9",
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Token {{6a739a16-d16d-4a06-93a5-3457da3c3d20}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"iconName": "flat_grey_ribbon",
|
||||||
|
"id": "1e047d02-a4a3-4cad-b4cc-123cc16c8398",
|
||||||
|
"launcherShortcut": true,
|
||||||
|
"method": "POST",
|
||||||
|
"name": "Linkding",
|
||||||
|
"quickSettingsTileShortcut": true,
|
||||||
|
"responseHandling": {
|
||||||
|
"failureOutput": "simple",
|
||||||
|
"id": "61fa9fc3-8b7a-47ce-b43c-f24618a65e1e",
|
||||||
|
"uiType": "toast"
|
||||||
|
},
|
||||||
|
"url": "{{ea2db14b-b9ca-45d8-8555-403271a38f5a}}/api/bookmarks/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variables": [
|
||||||
|
{
|
||||||
|
"id": "ea2db14b-b9ca-45d8-8555-403271a38f5a",
|
||||||
|
"key": "linkding_instance",
|
||||||
|
"value": "https://your.instance.tld.without.slashed.end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flags": 1,
|
||||||
|
"id": "b2953f61-b302-4c79-b90d-39858a06d9a6",
|
||||||
|
"key": "linkding_add_url",
|
||||||
|
"title": "Enter URL",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c360f61f-ce17-47b4-bea3-1d8c3913ca52",
|
||||||
|
"key": "linkding_tag",
|
||||||
|
"value": "single-tag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6a739a16-d16d-4a06-93a5-3457da3c3d20",
|
||||||
|
"key": "linkding_api_token",
|
||||||
|
"value": "your_token_from_integrations_tab"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 45
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "linkding",
|
"name": "linkding",
|
||||||
"version": "1.8.3",
|
"version": "1.8.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@@ -3,7 +3,7 @@ beautifulsoup4==4.7.1
|
|||||||
certifi==2019.6.16
|
certifi==2019.6.16
|
||||||
charset-normalizer==2.0.4
|
charset-normalizer==2.0.4
|
||||||
confusable-homoglyphs==3.2.0
|
confusable-homoglyphs==3.2.0
|
||||||
Django==3.2.6
|
Django==3.2.12
|
||||||
django-background-tasks==1.2.5
|
django-background-tasks==1.2.5
|
||||||
django-compat==1.0.15
|
django-compat==1.0.15
|
||||||
django-generate-secret-key==1.0.2
|
django-generate-secret-key==1.0.2
|
||||||
@@ -18,7 +18,7 @@ python-dateutil==2.8.1
|
|||||||
pytz==2021.1
|
pytz==2021.1
|
||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
soupsieve==1.9.2
|
soupsieve==1.9.2
|
||||||
sqlparse==0.4.1
|
sqlparse==0.4.2
|
||||||
supervisor==4.2.2
|
supervisor==4.2.2
|
||||||
typing-extensions==3.10.0.0
|
typing-extensions==3.10.0.0
|
||||||
urllib3==1.26.6
|
urllib3==1.26.6
|
||||||
|
@@ -4,7 +4,7 @@ certifi==2019.6.16
|
|||||||
charset-normalizer==2.0.4
|
charset-normalizer==2.0.4
|
||||||
confusable-homoglyphs==3.2.0
|
confusable-homoglyphs==3.2.0
|
||||||
coverage==5.5
|
coverage==5.5
|
||||||
Django==3.2.6
|
Django==3.2.12
|
||||||
django-appconf==1.0.4
|
django-appconf==1.0.4
|
||||||
django-background-tasks==1.2.5
|
django-background-tasks==1.2.5
|
||||||
django-compat==1.0.15
|
django-compat==1.0.15
|
||||||
@@ -26,7 +26,7 @@ requests==2.26.0
|
|||||||
rjsmin==1.1.0
|
rjsmin==1.1.0
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
soupsieve==1.9.2
|
soupsieve==1.9.2
|
||||||
sqlparse==0.4.1
|
sqlparse==0.4.2
|
||||||
typing-extensions==3.10.0.0
|
typing-extensions==3.10.0.0
|
||||||
urllib3==1.26.6
|
urllib3==1.26.6
|
||||||
waybackpy==2.4.3
|
waybackpy==2.4.3
|
||||||
|
@@ -25,11 +25,14 @@ urlpatterns = [
|
|||||||
extra_context=dict(allow_registration=ALLOW_REGISTRATION)),
|
extra_context=dict(allow_registration=ALLOW_REGISTRATION)),
|
||||||
name='login'),
|
name='login'),
|
||||||
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
|
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')),
|
path('', include('bookmarks.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
|
||||||
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
|
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
|
||||||
|
|
||||||
if ALLOW_REGISTRATION:
|
if ALLOW_REGISTRATION:
|
||||||
|
@@ -1 +1 @@
|
|||||||
1.8.3
|
1.8.6
|
||||||
|
Reference in New Issue
Block a user