mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-26 20:06:42 +02:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d39ce076ec | ||
![]() |
aa0258d3b6 | ||
![]() |
937858cf58 | ||
![]() |
8047ba6c63 | ||
![]() |
de903bc341 | ||
![]() |
c8fcc426b0 | ||
![]() |
eb915210d3 | ||
![]() |
ad9a0f84f2 | ||
![]() |
cc04a17e2f | ||
![]() |
69105d3d3c | ||
![]() |
c269d16855 | ||
![]() |
90ee3cdb94 | ||
![]() |
2c19266ef8 | ||
![]() |
048a8b1162 | ||
![]() |
2fb0bb1224 | ||
![]() |
3e48b22095 | ||
![]() |
9aa17d0528 | ||
![]() |
d643fca98f | ||
![]() |
f293fa15bc | ||
![]() |
f58434077b | ||
![]() |
59641e787c | ||
![]() |
0d36a3bb86 |
3
.coveragerc
Normal file
3
.coveragerc
Normal file
@@ -0,0 +1,3 @@
|
||||
[run]
|
||||
source = bookmarks
|
||||
omit = bookmarks/tests/*
|
8
.github/workflows/main.yaml
vendored
8
.github/workflows/main.yaml
vendored
@@ -12,7 +12,13 @@ jobs:
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install dependencies
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install Python dependencies
|
||||
run: pip install -r requirements.txt
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
- name: Run tests
|
||||
run: python manage.py test
|
||||
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.0 (17/08/2021)
|
||||
- Upgrade to Django 3
|
||||
- Bump other dependencies
|
||||
|
||||
---
|
||||
|
||||
## v1.6.5 (15/08/2021)
|
||||
- [**enhancement**] query with multiple hashtags very slow [#112](https://github.com/sissbruecker/linkding/issues/112)
|
||||
---
|
||||
|
||||
## v1.6.4 (13/05/2021)
|
||||
- Update dependencies for security fixes
|
||||
|
||||
---
|
||||
|
||||
## v1.6.3 (07/04/2021)
|
||||
- [**bug**] relative names use the wrong "today" after day change [#107](https://github.com/sissbruecker/linkding/issues/107)
|
||||
|
||||
|
@@ -47,6 +47,8 @@ EXPOSE 9090
|
||||
# Activate virtual env
|
||||
ENV VIRTUAL_ENV /opt/venv
|
||||
ENV PATH /opt/venv/bin:$PATH
|
||||
# Allow running containers as an an arbitrary user in the root group, to support deployment scenarios like OpenShift, Podman
|
||||
RUN ["chmod", "g+w", "."]
|
||||
# Run bootstrap logic
|
||||
RUN ["chmod", "+x", "./bootstrap.sh"]
|
||||
CMD ["./bootstrap.sh"]
|
||||
|
@@ -88,7 +88,7 @@ If you can not or don't want to use Docker you can install the application manua
|
||||
|
||||
### Hosting
|
||||
|
||||
The application runs in a web-server called [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) that is production-ready and that you can expose to the web. If you don't know how to configure your server to expose the application to the web there are several more steps involved. I can not support support the process here, but I can give some pointers on what to search for:
|
||||
The application runs in a web-server called [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) that is production-ready and that you can expose to the web. If you don't know how to configure your server to expose the application to the web there are several more steps involved. I can not support the process here, but I can give some pointers on what to search for:
|
||||
- first get the app running (described in this document)
|
||||
- open the port that the application is running on in your servers firewall
|
||||
- depending on your network configuration, forward the opened port in your network router, so that the application can be addressed from the internet using your public IP address and the opened port
|
||||
@@ -126,7 +126,7 @@ Note that any proxy servers that you are running in front of linkding may have t
|
||||
|
||||
## Development
|
||||
|
||||
The application is open source, so you are free to modify or contribute. The application is built using the Django web framework. You can get started by checking out the excellent Django docs: https://docs.djangoproject.com/en/3.0/. The `bookmarks` folder contains the actual bookmark application, `siteroot` is the Django root application. Other than that the code should be self-explanatory / standard Django stuff 🙂.
|
||||
The application is open source, so you are free to modify or contribute. The application is built using the Django web framework. You can get started by checking out the excellent Django docs: https://docs.djangoproject.com/en/3.2/. The `bookmarks` folder contains the actual bookmark application, `siteroot` is the Django root application. Other than that the code should be self-explanatory / standard Django stuff 🙂.
|
||||
|
||||
### Prerequisites
|
||||
- Python 3
|
||||
|
@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
|
||||
from django.db.models import Count, QuerySet
|
||||
from django.utils.translation import ngettext, gettext
|
||||
from rest_framework.authtoken.admin import TokenAdmin
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.authtoken.models import TokenProxy
|
||||
|
||||
from bookmarks.models import Bookmark, Tag, UserProfile
|
||||
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
|
||||
@@ -97,4 +97,4 @@ linkding_admin_site = LinkdingAdminSite()
|
||||
linkding_admin_site.register(Bookmark, AdminBookmark)
|
||||
linkding_admin_site.register(Tag, AdminTag)
|
||||
linkding_admin_site.register(User, AdminCustomUser)
|
||||
linkding_admin_site.register(Token, TokenAdmin)
|
||||
linkding_admin_site.register(TokenProxy, TokenAdmin)
|
||||
|
@@ -62,43 +62,17 @@ def _base_bookmarks_query(user: User, query_string: str) -> QuerySet:
|
||||
|
||||
|
||||
def query_bookmark_tags(user: User, query_string: str) -> QuerySet:
|
||||
return _base_bookmark_tags_query(user, query_string) \
|
||||
.filter(bookmark__is_archived=False) \
|
||||
.distinct()
|
||||
bookmarks_query = query_bookmarks(user, query_string)
|
||||
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
|
||||
return query_set.distinct()
|
||||
|
||||
|
||||
def query_archived_bookmark_tags(user: User, query_string: str) -> QuerySet:
|
||||
return _base_bookmark_tags_query(user, query_string) \
|
||||
.filter(bookmark__is_archived=True) \
|
||||
.distinct()
|
||||
bookmarks_query = query_archived_bookmarks(user, query_string)
|
||||
|
||||
|
||||
def _base_bookmark_tags_query(user: User, query_string: str) -> QuerySet:
|
||||
query_set = Tag.objects
|
||||
|
||||
# Filter for user
|
||||
query_set = query_set.filter(owner=user)
|
||||
|
||||
# Only show tags which have bookmarks
|
||||
query_set = query_set.filter(bookmark__isnull=False)
|
||||
|
||||
# Split query into search terms and tags
|
||||
query = _parse_query_string(query_string)
|
||||
|
||||
# Filter for search terms and tags
|
||||
for term in query['search_terms']:
|
||||
query_set = query_set.filter(
|
||||
Q(bookmark__title__contains=term)
|
||||
| Q(bookmark__description__contains=term)
|
||||
| Q(bookmark__website_title__contains=term)
|
||||
| Q(bookmark__website_description__contains=term)
|
||||
| Q(bookmark__url__contains=term)
|
||||
)
|
||||
|
||||
for tag_name in query['tag_names']:
|
||||
query_set = query_set.filter(
|
||||
bookmark__tags__name__iexact=tag_name
|
||||
)
|
||||
query_set = Tag.objects.filter(bookmark__in=bookmarks_query)
|
||||
|
||||
return query_set.distinct()
|
||||
|
||||
|
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
|
||||
from bookmarks.models import Bookmark, parse_tag_string
|
||||
from bookmarks.services.parser import parse, NetscapeBookmark
|
||||
@@ -45,7 +46,10 @@ def _import_bookmark_tag(netscape_bookmark: NetscapeBookmark, user: User):
|
||||
bookmark = _get_or_create_bookmark(netscape_bookmark.href, user)
|
||||
|
||||
bookmark.url = netscape_bookmark.href
|
||||
bookmark.date_added = datetime.utcfromtimestamp(int(netscape_bookmark.date_added)).astimezone()
|
||||
if netscape_bookmark.date_added:
|
||||
bookmark.date_added = datetime.utcfromtimestamp(int(netscape_bookmark.date_added)).astimezone()
|
||||
else:
|
||||
bookmark.date_added = timezone.now()
|
||||
bookmark.date_modified = bookmark.date_added
|
||||
bookmark.unread = False
|
||||
bookmark.title = netscape_bookmark.title
|
||||
@@ -53,6 +57,7 @@ def _import_bookmark_tag(netscape_bookmark: NetscapeBookmark, user: User):
|
||||
bookmark.description = netscape_bookmark.description
|
||||
bookmark.owner = user
|
||||
|
||||
bookmark.full_clean()
|
||||
bookmark.save()
|
||||
|
||||
# Set tags
|
||||
|
@@ -1,5 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
import pyparsing as pp
|
||||
|
||||
@@ -9,7 +8,7 @@ class NetscapeBookmark:
|
||||
href: str
|
||||
title: str
|
||||
description: str
|
||||
date_added: int
|
||||
date_added: str
|
||||
tag_string: str
|
||||
|
||||
|
||||
@@ -17,8 +16,7 @@ def extract_bookmark_link(tag):
|
||||
href = tag[0].href
|
||||
title = tag[0].text
|
||||
tag_string = tag[0].tags
|
||||
date_added_string = tag[0].add_date if tag[0].add_date else datetime.now().timestamp()
|
||||
date_added = int(date_added_string)
|
||||
date_added = tag[0].add_date
|
||||
|
||||
return {
|
||||
'href': href,
|
||||
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from charset_normalizer import from_bytes
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -33,5 +34,11 @@ def load_website_metadata(url: str):
|
||||
|
||||
|
||||
def load_page(url: str):
|
||||
r = requests.get(url)
|
||||
return r.text
|
||||
r = requests.get(url, timeout=10)
|
||||
|
||||
# Use charset_normalizer to determine encoding that best matches the response content
|
||||
# Several sites seem to specify the response encoding incorrectly, so we ignore it and use custom logic instead
|
||||
# This is different from Response.text which does respect the encoding specified in the response first,
|
||||
# before trying to determine one
|
||||
results = from_bytes(r.content)
|
||||
return str(results.best())
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
<ul class="bookmark-list">
|
||||
{% for bookmark in bookmarks %}
|
||||
<li>
|
||||
<li data-is-bookmark-item>
|
||||
<label class="form-checkbox bulk-edit-toggle">
|
||||
<input type="checkbox" name="bookmark_id" value="{{ bookmark.id }}">
|
||||
<i class="form-icon"></i>
|
||||
|
@@ -7,13 +7,13 @@
|
||||
{# Highlight first char of first tag in group #}
|
||||
{% if forloop.counter == 1 %}
|
||||
<a href="?{% append_query_param q=tag.name|hash_tag %}"
|
||||
class="mr-2">
|
||||
class="mr-2" data-is-tag-item>
|
||||
<span class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{# Render remaining tags normally #}
|
||||
<a href="?{% append_query_param q=tag.name|hash_tag %}"
|
||||
class="mr-2">
|
||||
class="mr-2" data-is-tag-item>
|
||||
<span>{{ tag.name }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
</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_token_changelist' %}">admin panel</a>. After deleting the token, a new one will be generated when you reload this settings page.</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>
|
||||
|
@@ -69,6 +69,15 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{# About section #}
|
||||
<section class="content-area">
|
||||
<h2>About</h2>
|
||||
<p>Version: {{ app_version }}</p>
|
||||
<p>
|
||||
Code: <a href="https://github.com/sissbruecker/linkding/"
|
||||
target="_blank">GitHub</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import random
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
@@ -16,12 +19,29 @@ class BookmarkFactoryMixin:
|
||||
|
||||
return self.user
|
||||
|
||||
def setup_bookmark(self, is_archived: bool = False, tags: [Tag] = [], user: User = None):
|
||||
def setup_bookmark(self,
|
||||
is_archived: bool = False,
|
||||
tags=None,
|
||||
user: User = None,
|
||||
url: str = '',
|
||||
title: str = '',
|
||||
description: str = '',
|
||||
website_title: str = '',
|
||||
website_description: str = '',
|
||||
):
|
||||
if tags is None:
|
||||
tags = []
|
||||
if user is None:
|
||||
user = self.get_or_create_test_user()
|
||||
unique_id = get_random_string(length=32)
|
||||
if not url:
|
||||
unique_id = get_random_string(length=32)
|
||||
url = 'https://example.com/' + unique_id
|
||||
bookmark = Bookmark(
|
||||
url='https://example.com/' + unique_id,
|
||||
url=url,
|
||||
title=title,
|
||||
description=description,
|
||||
website_title=website_title,
|
||||
website_description=website_description,
|
||||
date_added=timezone.now(),
|
||||
date_modified=timezone.now(),
|
||||
owner=user,
|
||||
@@ -33,10 +53,11 @@ class BookmarkFactoryMixin:
|
||||
bookmark.save()
|
||||
return bookmark
|
||||
|
||||
def setup_tag(self, user: User = None):
|
||||
def setup_tag(self, user: User = None, name: str = ''):
|
||||
if user is None:
|
||||
user = self.get_or_create_test_user()
|
||||
name = get_random_string(length=32)
|
||||
if not name:
|
||||
name = get_random_string(length=32)
|
||||
tag = Tag(name=name, date_added=timezone.now(), owner=user)
|
||||
tag.save()
|
||||
return tag
|
||||
@@ -62,3 +83,49 @@ class LinkdingApiTestCase(APITestCase):
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
return response
|
||||
|
||||
|
||||
_words = [
|
||||
'quasi',
|
||||
'consequatur',
|
||||
'necessitatibus',
|
||||
'debitis',
|
||||
'quod',
|
||||
'vero',
|
||||
'qui',
|
||||
'commodi',
|
||||
'quod',
|
||||
'odio',
|
||||
'aliquam',
|
||||
'veniam',
|
||||
'architecto',
|
||||
'consequatur',
|
||||
'autem',
|
||||
'qui',
|
||||
'iste',
|
||||
'asperiores',
|
||||
'soluta',
|
||||
'et',
|
||||
]
|
||||
|
||||
|
||||
def random_sentence(num_words: int = None, including_word: str = ''):
|
||||
if num_words is None:
|
||||
num_words = random.randint(5, 10)
|
||||
selected_words = random.choices(_words, k=num_words)
|
||||
if including_word:
|
||||
selected_words.append(including_word)
|
||||
random.shuffle(selected_words)
|
||||
|
||||
return ' '.join(selected_words)
|
||||
|
||||
|
||||
def disable_logging(f):
|
||||
def wrapper(*args):
|
||||
logging.disable(logging.CRITICAL)
|
||||
result = f(*args)
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
BIN
bookmarks/tests/resources/invalid_import_file.png
Normal file
BIN
bookmarks/tests/resources/invalid_import_file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
20
bookmarks/tests/resources/simple_valid_import_file.html
Normal file
20
bookmarks/tests/resources/simple_valid_import_file.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
|
||||
<H1>Bookmarks</H1>
|
||||
|
||||
<DL><p>
|
||||
|
||||
<DT><A HREF="https://example.com/1" ADD_DATE="1616337559" PRIVATE="0" TOREAD="0" TAGS="tag1">test title 1</A>
|
||||
<DD>test description 1
|
||||
|
||||
<DT><A HREF="https://example.com/2" ADD_DATE="1616337559" PRIVATE="0" TOREAD="0" TAGS="tag2">test title 2</A>
|
||||
<DD>test description 2
|
||||
|
||||
<DT><A HREF="https://example.com/3" ADD_DATE="1616337559" PRIVATE="0" TOREAD="0" TAGS="tag3">test title 3</A>
|
||||
<DD>test description 3
|
||||
|
||||
</DL><p>
|
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
|
||||
<H1>Bookmarks</H1>
|
||||
|
||||
<DL><p>
|
||||
|
||||
<DT><A HREF="https://example.com/1" ADD_DATE="invaliddate" PRIVATE="0" TOREAD="0" TAGS="tag1">test title 1</A>
|
||||
<DD>test description 1
|
||||
|
||||
<DT><A HREF="https://example.com/2" ADD_DATE="1616337559" PRIVATE="0" TOREAD="0" TAGS="tag2">test title 2</A>
|
||||
<DD>test description 2
|
||||
|
||||
<DT><A HREF="https://example.com/3" ADD_DATE="1616337559" PRIVATE="0" TOREAD="0" TAGS="tag3">test title 3</A>
|
||||
<DD>test description 3
|
||||
|
||||
</DL><p>
|
35
bookmarks/tests/test_bookmark_archive_view.py
Normal file
35
bookmarks/tests/test_bookmark_archive_view.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkArchiveViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def test_should_archive_bookmark(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
self.client.get(reverse('bookmarks:archive', args=[bookmark.id]))
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertTrue(bookmark.is_archived)
|
||||
|
||||
def test_should_redirect_to_index(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archive', args=[bookmark.id]))
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
|
||||
def test_should_redirect_to_return_url_when_specified(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
response = self.client.get(
|
||||
reverse('bookmarks:archive', args=[bookmark.id]) + '?return_url=' + reverse('bookmarks:close')
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
132
bookmarks/tests/test_bookmark_archived_view.py
Normal file
132
bookmarks/tests/test_bookmark_archived_view.py
Normal file
@@ -0,0 +1,132 @@
|
||||
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.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertVisibleBookmarks(self, response, bookmarks: [Bookmark]):
|
||||
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),
|
||||
html
|
||||
)
|
||||
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: [Bookmark]):
|
||||
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),
|
||||
html,
|
||||
count=0
|
||||
)
|
||||
|
||||
def assertVisibleTags(self, response, tags: [Tag]):
|
||||
self.assertContains(response, 'data-is-tag-item', count=len(tags))
|
||||
|
||||
for tag in tags:
|
||||
self.assertContains(response, tag.name)
|
||||
|
||||
def assertInvisibleTags(self, response, tags: [Tag]):
|
||||
for tag in tags:
|
||||
self.assertNotContains(response, tag.name)
|
||||
|
||||
def test_should_list_archived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(is_archived=True)
|
||||
]
|
||||
invisible_bookmarks = [
|
||||
self.setup_bookmark(is_archived=False),
|
||||
self.setup_bookmark(is_archived=True, user=other_user),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
|
||||
self.assertContains(response, '<ul class="bookmark-list">') # Should render list
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_bookmarks_matching_query(self):
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(is_archived=True, title='searchvalue'),
|
||||
self.setup_bookmark(is_archived=True, title='searchvalue'),
|
||||
self.setup_bookmark(is_archived=True, title='searchvalue')
|
||||
]
|
||||
invisible_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') + '?q=searchvalue')
|
||||
|
||||
self.assertContains(response, '<ul class="bookmark-list">') # Should render list
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_tags_for_archived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
visible_tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
invisible_tags = [
|
||||
self.setup_tag(), # unused tag
|
||||
self.setup_tag(), # used in archived bookmark
|
||||
self.setup_tag(user=other_user), # belongs to other user
|
||||
]
|
||||
|
||||
# Assign tags to some bookmarks with duplicates
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[0]])
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[0]])
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[1]])
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[1]])
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[2]])
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[2]])
|
||||
|
||||
self.setup_bookmark(is_archived=False, tags=[invisible_tags[1]])
|
||||
self.setup_bookmark(is_archived=True, tags=[invisible_tags[2]], user=other_user)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived'))
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_list_tags_for_bookmarks_matching_query(self):
|
||||
visible_tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
invisible_tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[0]], title='searchvalue'),
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[1]], title='searchvalue')
|
||||
self.setup_bookmark(is_archived=True, tags=[visible_tags[2]], title='searchvalue')
|
||||
self.setup_bookmark(is_archived=True, tags=[invisible_tags[0]])
|
||||
self.setup_bookmark(is_archived=True, tags=[invisible_tags[1]])
|
||||
self.setup_bookmark(is_archived=True, tags=[invisible_tags[2]])
|
||||
|
||||
response = self.client.get(reverse('bookmarks:archived') + '?q=searchvalue')
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
@@ -6,7 +6,7 @@ from bookmarks.models import Bookmark
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BulkEditIntegrationTests(TestCase, BookmarkFactoryMixin):
|
||||
class BookmarkBulkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
97
bookmarks/tests/test_bookmark_edit_view.py
Normal file
97
bookmarks/tests/test_bookmark_edit_view.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import build_tag_string
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def create_form_data(self, overrides=None):
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
form_data = {
|
||||
'url': 'http://example.com/edited',
|
||||
'tag_string': 'editedtag1 editedtag2',
|
||||
'title': 'edited title',
|
||||
'description': 'edited description',
|
||||
'return_url': reverse('bookmarks:index'),
|
||||
}
|
||||
return {**form_data, **overrides}
|
||||
|
||||
def test_should_edit_bookmark(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
form_data = self.create_form_data({'id': bookmark.id})
|
||||
|
||||
self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertEqual(bookmark.owner, self.user)
|
||||
self.assertEqual(bookmark.url, form_data['url'])
|
||||
self.assertEqual(bookmark.title, form_data['title'])
|
||||
self.assertEqual(bookmark.description, form_data['description'])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
self.assertEqual(bookmark.tags.all()[0].name, 'editedtag1')
|
||||
self.assertEqual(bookmark.tags.all()[1].name, 'editedtag2')
|
||||
|
||||
def test_should_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):
|
||||
tag1 = self.setup_tag()
|
||||
tag2 = self.setup_tag()
|
||||
bookmark = self.setup_bookmark(tags=[tag1, tag2], title='edited title', description='edited description')
|
||||
|
||||
response = self.client.get(reverse('bookmarks:edit', args=[bookmark.id]))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('<input type="text" name="url" '
|
||||
'value="{0}" placeholder=" " '
|
||||
'autofocus class="form-input" required '
|
||||
'id="id_url">'.format(bookmark.url),
|
||||
html)
|
||||
|
||||
tag_string = build_tag_string(bookmark.tag_names, ' ')
|
||||
self.assertInHTML('<input type="text" name="tag_string" '
|
||||
'value="{0}" autocomplete="off" '
|
||||
'class="form-input" '
|
||||
'id="id_tag_string">'.format(tag_string),
|
||||
html)
|
||||
|
||||
self.assertInHTML('<input type="text" name="title" maxlength="512" '
|
||||
'autocomplete="off" class="form-input" '
|
||||
'value="{0}" id="id_title">'.format(bookmark.title),
|
||||
html)
|
||||
|
||||
self.assertInHTML('<textarea name="description" cols="40" rows="4" class="form-input" id="id_description">{0}'
|
||||
'</textarea>'.format(bookmark.description),
|
||||
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):
|
||||
bookmark = self.setup_bookmark()
|
||||
form_data = self.create_form_data({'return_url': reverse('bookmarks:close')})
|
||||
|
||||
response = self.client.post(reverse('bookmarks:edit', args=[bookmark.id]), form_data)
|
||||
|
||||
self.assertRedirects(response, form_data['return_url'])
|
132
bookmarks/tests/test_bookmark_index_view.py
Normal file
132
bookmarks/tests/test_bookmark_index_view.py
Normal file
@@ -0,0 +1,132 @@
|
||||
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.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertVisibleBookmarks(self, response, bookmarks: [Bookmark]):
|
||||
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),
|
||||
html
|
||||
)
|
||||
|
||||
def assertInvisibleBookmarks(self, response, bookmarks: [Bookmark]):
|
||||
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),
|
||||
html,
|
||||
count=0
|
||||
)
|
||||
|
||||
def assertVisibleTags(self, response, tags: [Tag]):
|
||||
self.assertContains(response, 'data-is-tag-item', count=len(tags))
|
||||
|
||||
for tag in tags:
|
||||
self.assertContains(response, tag.name)
|
||||
|
||||
def assertInvisibleTags(self, response, tags: [Tag]):
|
||||
for tag in tags:
|
||||
self.assertNotContains(response, tag.name)
|
||||
|
||||
def test_should_list_unarchived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark()
|
||||
]
|
||||
invisible_bookmarks = [
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(user=other_user),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
|
||||
self.assertContains(response, '<ul class="bookmark-list">') # Should render list
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_bookmarks_matching_query(self):
|
||||
visible_bookmarks = [
|
||||
self.setup_bookmark(title='searchvalue'),
|
||||
self.setup_bookmark(title='searchvalue'),
|
||||
self.setup_bookmark(title='searchvalue')
|
||||
]
|
||||
invisible_bookmarks = [
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark()
|
||||
]
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + '?q=searchvalue')
|
||||
|
||||
self.assertContains(response, '<ul class="bookmark-list">') # Should render list
|
||||
self.assertVisibleBookmarks(response, visible_bookmarks)
|
||||
self.assertInvisibleBookmarks(response, invisible_bookmarks)
|
||||
|
||||
def test_should_list_tags_for_unarchived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
visible_tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
invisible_tags = [
|
||||
self.setup_tag(), # unused tag
|
||||
self.setup_tag(), # used in archived bookmark
|
||||
self.setup_tag(user=other_user), # belongs to other user
|
||||
]
|
||||
|
||||
# Assign tags to some bookmarks with duplicates
|
||||
self.setup_bookmark(tags=[visible_tags[0]])
|
||||
self.setup_bookmark(tags=[visible_tags[0]])
|
||||
self.setup_bookmark(tags=[visible_tags[1]])
|
||||
self.setup_bookmark(tags=[visible_tags[1]])
|
||||
self.setup_bookmark(tags=[visible_tags[2]])
|
||||
self.setup_bookmark(tags=[visible_tags[2]])
|
||||
|
||||
self.setup_bookmark(tags=[invisible_tags[1]], is_archived=True)
|
||||
self.setup_bookmark(tags=[invisible_tags[2]], user=other_user)
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index'))
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
||||
|
||||
def test_should_list_tags_for_bookmarks_matching_query(self):
|
||||
visible_tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
invisible_tags = [
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
self.setup_tag(),
|
||||
]
|
||||
|
||||
self.setup_bookmark(tags=[visible_tags[0]], title='searchvalue'),
|
||||
self.setup_bookmark(tags=[visible_tags[1]], title='searchvalue')
|
||||
self.setup_bookmark(tags=[visible_tags[2]], title='searchvalue')
|
||||
self.setup_bookmark(tags=[invisible_tags[0]])
|
||||
self.setup_bookmark(tags=[invisible_tags[1]])
|
||||
self.setup_bookmark(tags=[invisible_tags[2]])
|
||||
|
||||
response = self.client.get(reverse('bookmarks:index') + '?q=searchvalue')
|
||||
|
||||
self.assertVisibleTags(response, visible_tags)
|
||||
self.assertInvisibleTags(response, invisible_tags)
|
81
bookmarks/tests/test_bookmark_new_view.py
Normal file
81
bookmarks/tests/test_bookmark_new_view.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import Bookmark
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def create_form_data(self, overrides=None):
|
||||
if overrides is None:
|
||||
overrides = {}
|
||||
form_data = {
|
||||
'url': 'http://example.com',
|
||||
'tag_string': 'tag1 tag2',
|
||||
'title': 'test title',
|
||||
'description': 'test description',
|
||||
'auto_close': '',
|
||||
}
|
||||
return {**form_data, **overrides}
|
||||
|
||||
def test_should_create_new_bookmark(self):
|
||||
form_data = self.create_form_data()
|
||||
|
||||
self.client.post(reverse('bookmarks:new'), form_data)
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 1)
|
||||
|
||||
bookmark = Bookmark.objects.first()
|
||||
self.assertEqual(bookmark.owner, self.user)
|
||||
self.assertEqual(bookmark.url, form_data['url'])
|
||||
self.assertEqual(bookmark.title, form_data['title'])
|
||||
self.assertEqual(bookmark.description, form_data['description'])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
self.assertEqual(bookmark.tags.all()[0].name, 'tag1')
|
||||
self.assertEqual(bookmark.tags.all()[1].name, 'tag2')
|
||||
|
||||
def test_should_prefill_url_from_url_parameter(self):
|
||||
response = self.client.get(reverse('bookmarks:new') + '?url=http://example.com')
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(
|
||||
'<input type="text" name="url" value="http://example.com" '
|
||||
'placeholder=" " autofocus class="form-input" required '
|
||||
'id="id_url">',
|
||||
html)
|
||||
|
||||
def test_should_enable_auto_close_when_specified_in_url_parameter(self):
|
||||
response = self.client.get(
|
||||
reverse('bookmarks:new') + '?auto_close')
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(
|
||||
'<input type="hidden" name="auto_close" value="true" '
|
||||
'id="id_auto_close">',
|
||||
html)
|
||||
|
||||
def test_should_not_enable_auto_close_when_not_specified_in_url_parameter(
|
||||
self):
|
||||
response = self.client.get(reverse('bookmarks:new'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML('<input type="hidden" name="auto_close" id="id_auto_close">',html)
|
||||
|
||||
def test_should_redirect_to_index_view(self):
|
||||
form_data = self.create_form_data()
|
||||
|
||||
response = self.client.post(reverse('bookmarks:new'), form_data)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
|
||||
def test_auto_close_should_redirect_to_close_view(self):
|
||||
form_data = self.create_form_data({'auto_close': 'true'})
|
||||
|
||||
response = self.client.post(reverse('bookmarks:new'), form_data)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
35
bookmarks/tests/test_bookmark_remove_view.py
Normal file
35
bookmarks/tests/test_bookmark_remove_view.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.models import Bookmark
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkRemoveViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def test_should_delete_bookmark(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
self.client.get(reverse('bookmarks:remove', args=[bookmark.id]))
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 0)
|
||||
|
||||
def test_should_redirect_to_index(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
response = self.client.get(reverse('bookmarks:remove', args=[bookmark.id]))
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:index'))
|
||||
|
||||
def test_should_redirect_to_return_url_when_specified(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
response = self.client.get(
|
||||
reverse('bookmarks:remove', args=[bookmark.id]) + '?return_url=' + reverse('bookmarks:close')
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
35
bookmarks/tests/test_bookmark_unarchive_view.py
Normal file
35
bookmarks/tests/test_bookmark_unarchive_view.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkUnarchiveViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def test_should_unarchive_bookmark(self):
|
||||
bookmark = self.setup_bookmark(is_archived=True)
|
||||
|
||||
self.client.get(reverse('bookmarks:unarchive', args=[bookmark.id]))
|
||||
bookmark.refresh_from_db()
|
||||
|
||||
self.assertFalse(bookmark.is_archived)
|
||||
|
||||
def test_should_redirect_to_archive(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
response = self.client.get(reverse('bookmarks:unarchive', args=[bookmark.id]))
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:archived'))
|
||||
|
||||
def test_should_redirect_to_return_url_when_specified(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
response = self.client.get(
|
||||
reverse('bookmarks:unarchive', args=[bookmark.id]) + '?return_url=' + reverse('bookmarks:close')
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:close'))
|
@@ -34,6 +34,27 @@ class BookmarkValidationTestCase(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_user('testuser', 'test@example.com', 'password123')
|
||||
|
||||
def test_bookmark_model_should_not_allow_missing_url(self):
|
||||
bookmark = Bookmark(
|
||||
date_added=datetime.datetime.now(),
|
||||
date_modified=datetime.datetime.now(),
|
||||
owner=self.user
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
bookmark.full_clean()
|
||||
|
||||
def test_bookmark_model_should_not_allow_empty_url(self):
|
||||
bookmark = Bookmark(
|
||||
url='',
|
||||
date_added=datetime.datetime.now(),
|
||||
date_modified=datetime.datetime.now(),
|
||||
owner=self.user
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
bookmark.full_clean()
|
||||
|
||||
@override_settings(LD_DISABLE_URL_VALIDATION=False)
|
||||
def test_bookmark_model_should_validate_url_if_not_disabled_in_settings(self):
|
||||
self._run_bookmark_model_url_validity_checks(ENABLED_URL_VALIDATION_TEST_CASES)
|
||||
|
@@ -4,8 +4,8 @@ from django.template import Template, RequestContext
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.utils import timezone, formats
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
from bookmarks.models import UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class BookmarkListTagTest(TestCase, BookmarkFactoryMixin):
|
||||
|
33
bookmarks/tests/test_importer.py
Normal file
33
bookmarks/tests/test_importer.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from bookmarks.services.importer import import_netscape_html
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging
|
||||
|
||||
|
||||
class ImporterTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def create_import_html(self, bookmark_tags_string: str):
|
||||
return f'''
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
<DL><p>
|
||||
{bookmark_tags_string}
|
||||
</DL><p>
|
||||
'''
|
||||
|
||||
@disable_logging
|
||||
def test_validate_empty_or_missing_bookmark_url(self):
|
||||
test_html = self.create_import_html(f'''
|
||||
<!-- Empty URL -->
|
||||
<DT><A HREF="" ADD_DATE="1616337559" PRIVATE="0" TOREAD="0" TAGS="tag3">Empty URL</A>
|
||||
<DD>Empty URL
|
||||
<!-- Missing URL -->
|
||||
<DT><A ADD_DATE="1616337559" PRIVATE="0" TOREAD="0" TAGS="tag3">Missing URL</A>
|
||||
<DD>Missing URL
|
||||
''')
|
||||
|
||||
import_result = import_netscape_html(test_html, self.get_or_create_test_user())
|
||||
|
||||
self.assertEqual(import_result.success, 0)
|
@@ -1,6 +1,6 @@
|
||||
from django.core.paginator import Paginator
|
||||
from django.test import SimpleTestCase, RequestFactory
|
||||
from django.template import Template, RequestContext
|
||||
from django.test import SimpleTestCase, RequestFactory
|
||||
|
||||
|
||||
class PaginationTagTest(SimpleTestCase):
|
||||
|
@@ -1,14 +1,223 @@
|
||||
import operator
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import QuerySet
|
||||
from django.test import TestCase
|
||||
|
||||
from bookmarks import queries
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
from bookmarks.models import Bookmark
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, random_sentence
|
||||
from bookmarks.utils import unique
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setup_bookmark_search_data(self) -> None:
|
||||
tag1 = self.setup_tag(name='tag1')
|
||||
tag2 = self.setup_tag(name='tag2')
|
||||
self.setup_tag(name='unused_tag1')
|
||||
|
||||
self.other_bookmarks = [
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark(),
|
||||
]
|
||||
self.term1_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1'),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1')),
|
||||
self.setup_bookmark(description=random_sentence(including_word='term1')),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='term1')),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='term1')),
|
||||
]
|
||||
self.term1_term2_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1/term2'),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1'),
|
||||
description=random_sentence(including_word='term2')),
|
||||
self.setup_bookmark(description=random_sentence(including_word='term1'),
|
||||
title=random_sentence(including_word='term2')),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='term1'),
|
||||
title=random_sentence(including_word='term2')),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='term1'),
|
||||
title=random_sentence(including_word='term2')),
|
||||
]
|
||||
self.tag1_bookmarks = [
|
||||
self.setup_bookmark(tags=[tag1]),
|
||||
self.setup_bookmark(title=random_sentence(), tags=[tag1]),
|
||||
self.setup_bookmark(description=random_sentence(), tags=[tag1]),
|
||||
self.setup_bookmark(website_title=random_sentence(), tags=[tag1]),
|
||||
self.setup_bookmark(website_description=random_sentence(), tags=[tag1]),
|
||||
]
|
||||
self.term1_tag1_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1', tags=[tag1]),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1]),
|
||||
self.setup_bookmark(description=random_sentence(including_word='term1'), tags=[tag1]),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='term1'), tags=[tag1]),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='term1'), tags=[tag1]),
|
||||
]
|
||||
self.tag2_bookmarks = [
|
||||
self.setup_bookmark(tags=[tag2]),
|
||||
]
|
||||
self.tag1_tag2_bookmarks = [
|
||||
self.setup_bookmark(tags=[tag1, tag2]),
|
||||
]
|
||||
|
||||
def setup_tag_search_data(self):
|
||||
tag1 = self.setup_tag(name='tag1')
|
||||
tag2 = self.setup_tag(name='tag2')
|
||||
self.setup_tag(name='unused_tag1')
|
||||
|
||||
self.other_bookmarks = [
|
||||
self.setup_bookmark(tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(tags=[self.setup_tag()]),
|
||||
]
|
||||
self.term1_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1', tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(description=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='term1'), tags=[self.setup_tag()]),
|
||||
]
|
||||
self.term1_term2_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1/term2', tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1'),
|
||||
description=random_sentence(including_word='term2'),
|
||||
tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(description=random_sentence(including_word='term1'),
|
||||
title=random_sentence(including_word='term2'),
|
||||
tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='term1'),
|
||||
title=random_sentence(including_word='term2'),
|
||||
tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='term1'),
|
||||
title=random_sentence(including_word='term2'),
|
||||
tags=[self.setup_tag()]),
|
||||
]
|
||||
self.tag1_bookmarks = [
|
||||
self.setup_bookmark(tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(title=random_sentence(), tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(description=random_sentence(), tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(website_title=random_sentence(), tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(website_description=random_sentence(), tags=[tag1, self.setup_tag()]),
|
||||
]
|
||||
self.term1_tag1_bookmarks = [
|
||||
self.setup_bookmark(url='http://example.com/term1', tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(title=random_sentence(including_word='term1'), tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(description=random_sentence(including_word='term1'), tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(website_title=random_sentence(including_word='term1'), tags=[tag1, self.setup_tag()]),
|
||||
self.setup_bookmark(website_description=random_sentence(including_word='term1'),
|
||||
tags=[tag1, self.setup_tag()]),
|
||||
]
|
||||
self.tag2_bookmarks = [
|
||||
self.setup_bookmark(tags=[tag2, self.setup_tag()]),
|
||||
]
|
||||
self.tag1_tag2_bookmarks = [
|
||||
self.setup_bookmark(tags=[tag1, tag2, self.setup_tag()]),
|
||||
]
|
||||
|
||||
def get_tags_from_bookmarks(self, bookmarks: [Bookmark]):
|
||||
all_tags = []
|
||||
for bookmark in bookmarks:
|
||||
all_tags = all_tags + list(bookmark.tags.all())
|
||||
return all_tags
|
||||
|
||||
def assertQueryResult(self, query: QuerySet, item_lists: [[any]]):
|
||||
expected_items = []
|
||||
for item_list in item_lists:
|
||||
expected_items = expected_items + item_list
|
||||
|
||||
expected_items = unique(expected_items, operator.attrgetter('id'))
|
||||
|
||||
self.assertCountEqual(list(query), expected_items)
|
||||
|
||||
def test_query_bookmarks_should_return_all_for_empty_query(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '')
|
||||
self.assertQueryResult(query, [
|
||||
self.other_bookmarks,
|
||||
self.term1_bookmarks,
|
||||
self.term1_term2_bookmarks,
|
||||
self.tag1_bookmarks,
|
||||
self.term1_tag1_bookmarks,
|
||||
self.tag2_bookmarks,
|
||||
self.tag1_tag2_bookmarks
|
||||
])
|
||||
|
||||
def test_query_bookmarks_should_search_single_term(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1')
|
||||
self.assertQueryResult(query, [
|
||||
self.term1_bookmarks,
|
||||
self.term1_term2_bookmarks,
|
||||
self.term1_tag1_bookmarks
|
||||
])
|
||||
|
||||
def test_query_bookmarks_should_search_multiple_terms(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term2 term1')
|
||||
|
||||
self.assertQueryResult(query, [self.term1_term2_bookmarks])
|
||||
|
||||
def test_query_bookmarks_should_search_single_tag(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#tag1')
|
||||
|
||||
self.assertQueryResult(query, [self.tag1_bookmarks, self.tag1_tag2_bookmarks, self.term1_tag1_bookmarks])
|
||||
|
||||
def test_query_bookmarks_should_search_multiple_tags(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#tag1 #tag2')
|
||||
|
||||
self.assertQueryResult(query, [self.tag1_tag2_bookmarks])
|
||||
|
||||
def test_query_bookmarks_should_search_multiple_tags_ignoring_casing(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#Tag1 #TAG2')
|
||||
|
||||
self.assertQueryResult(query, [self.tag1_tag2_bookmarks])
|
||||
|
||||
def test_query_bookmarks_should_search_terms_and_tags_combined(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1 #tag1')
|
||||
|
||||
self.assertQueryResult(query, [self.term1_tag1_bookmarks])
|
||||
|
||||
def test_query_bookmarks_should_return_no_matches(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1 term3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1 #tag2')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#tag3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag combined with tag that is used
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '#tag1 #unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag combined with term that is used
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), 'term1 #unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
def test_query_bookmarks_should_not_return_archived_bookmarks(self):
|
||||
bookmark1 = self.setup_bookmark()
|
||||
bookmark2 = self.setup_bookmark()
|
||||
@@ -18,7 +227,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
query = queries.query_bookmarks(self.get_or_create_test_user(), '')
|
||||
|
||||
self.assertCountEqual([bookmark1, bookmark2], list(query))
|
||||
self.assertQueryResult(query, [[bookmark1, bookmark2]])
|
||||
|
||||
def test_query_archived_bookmarks_should_not_return_unarchived_bookmarks(self):
|
||||
bookmark1 = self.setup_bookmark(is_archived=True)
|
||||
@@ -29,7 +238,156 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
query = queries.query_archived_bookmarks(self.get_or_create_test_user(), '')
|
||||
|
||||
self.assertCountEqual([bookmark1, bookmark2], list(query))
|
||||
self.assertQueryResult(query, [[bookmark1, bookmark2]])
|
||||
|
||||
def test_query_bookmarks_should_only_return_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
owned_bookmarks = [
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark(),
|
||||
self.setup_bookmark(),
|
||||
]
|
||||
self.setup_bookmark(user=other_user)
|
||||
self.setup_bookmark(user=other_user)
|
||||
self.setup_bookmark(user=other_user)
|
||||
|
||||
query = queries.query_bookmarks(self.user, '')
|
||||
|
||||
self.assertQueryResult(query, [owned_bookmarks])
|
||||
|
||||
def test_query_archived_bookmarks_should_only_return_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
owned_bookmarks = [
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(is_archived=True),
|
||||
self.setup_bookmark(is_archived=True),
|
||||
]
|
||||
self.setup_bookmark(is_archived=True, user=other_user)
|
||||
self.setup_bookmark(is_archived=True, user=other_user)
|
||||
self.setup_bookmark(is_archived=True, user=other_user)
|
||||
|
||||
query = queries.query_archived_bookmarks(self.user, '')
|
||||
|
||||
self.assertQueryResult(query, [owned_bookmarks])
|
||||
|
||||
def test_query_bookmarks_should_use_tag_projection(self):
|
||||
self.setup_bookmark_search_data()
|
||||
|
||||
# Test projection on bookmarks with tags
|
||||
query = queries.query_bookmarks(self.user, '#tag1 #tag2')
|
||||
|
||||
for bookmark in query:
|
||||
self.assertEqual(bookmark.tag_count, 2)
|
||||
self.assertEqual(bookmark.tag_string, 'tag1,tag2')
|
||||
self.assertTrue(bookmark.tag_projection)
|
||||
|
||||
# Test projection on bookmarks without tags
|
||||
query = queries.query_bookmarks(self.user, 'term2')
|
||||
|
||||
for bookmark in query:
|
||||
self.assertEqual(bookmark.tag_count, 0)
|
||||
self.assertEqual(bookmark.tag_string, None)
|
||||
self.assertTrue(bookmark.tag_projection)
|
||||
|
||||
def test_query_bookmark_tags_should_return_all_tags_for_empty_query(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, '')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.other_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.term1_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.tag1_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.tag2_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_should_search_single_term(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, 'term1')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.term1_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_should_search_multiple_terms(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, 'term2 term1')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.term1_term2_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_should_search_single_tag(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, '#tag1')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_should_search_multiple_tags(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, '#tag1 #tag2')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_should_search_multiple_tags_ignoring_casing(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, '#Tag1 #TAG2')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.tag1_tag2_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_should_search_term_and_tag_combined(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, 'term1 #tag1')
|
||||
|
||||
self.assertQueryResult(query, [
|
||||
self.get_tags_from_bookmarks(self.term1_tag1_bookmarks),
|
||||
])
|
||||
|
||||
def test_query_bookmark_tags_should_return_no_matches(self):
|
||||
self.setup_tag_search_data()
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), 'term3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), 'term1 term3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), 'term1 #tag2')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '#tag3')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '#unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag combined with tag that is used
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '#tag1 #unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
# Unused tag combined with term that is used
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), 'term1 #unused_tag1')
|
||||
self.assertQueryResult(query, [])
|
||||
|
||||
def test_query_bookmark_tags_should_return_tags_for_unarchived_bookmarks_only(self):
|
||||
tag1 = self.setup_tag()
|
||||
@@ -40,7 +398,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '')
|
||||
|
||||
self.assertCountEqual([tag1], list(query))
|
||||
self.assertQueryResult(query, [[tag1]])
|
||||
|
||||
def test_query_bookmark_tags_should_return_distinct_tags(self):
|
||||
tag = self.setup_tag()
|
||||
@@ -50,7 +408,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
query = queries.query_bookmark_tags(self.get_or_create_test_user(), '')
|
||||
|
||||
self.assertCountEqual([tag], list(query))
|
||||
self.assertQueryResult(query, [[tag]])
|
||||
|
||||
def test_query_archived_bookmark_tags_should_return_tags_for_archived_bookmarks_only(self):
|
||||
tag1 = self.setup_tag()
|
||||
@@ -61,7 +419,7 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
query = queries.query_archived_bookmark_tags(self.get_or_create_test_user(), '')
|
||||
|
||||
self.assertCountEqual([tag2], list(query))
|
||||
self.assertQueryResult(query, [[tag2]])
|
||||
|
||||
def test_query_archived_bookmark_tags_should_return_distinct_tags(self):
|
||||
tag = self.setup_tag()
|
||||
@@ -71,4 +429,34 @@ class QueriesTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
query = queries.query_archived_bookmark_tags(self.get_or_create_test_user(), '')
|
||||
|
||||
self.assertCountEqual([tag], list(query))
|
||||
self.assertQueryResult(query, [[tag]])
|
||||
|
||||
def test_query_bookmark_tags_should_only_return_user_owned_tags(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
owned_bookmarks = [
|
||||
self.setup_bookmark(tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(tags=[self.setup_tag()]),
|
||||
]
|
||||
self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)])
|
||||
self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)])
|
||||
self.setup_bookmark(user=other_user, tags=[self.setup_tag(user=other_user)])
|
||||
|
||||
query = queries.query_bookmark_tags(self.user, '')
|
||||
|
||||
self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
|
||||
|
||||
def test_query_archived_bookmark_tags_should_only_return_user_owned_tags(self):
|
||||
other_user = User.objects.create_user('otheruser', 'otheruser@example.com', 'password123')
|
||||
owned_bookmarks = [
|
||||
self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]),
|
||||
self.setup_bookmark(is_archived=True, tags=[self.setup_tag()]),
|
||||
]
|
||||
self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)])
|
||||
self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)])
|
||||
self.setup_bookmark(is_archived=True, user=other_user, tags=[self.setup_tag(user=other_user)])
|
||||
|
||||
query = queries.query_archived_bookmark_tags(self.user, '')
|
||||
|
||||
self.assertQueryResult(query, [self.get_tags_from_bookmarks(owned_bookmarks)])
|
||||
|
40
bookmarks/tests/test_settings_api_view.py
Normal file
40
bookmarks/tests/test_settings_api_view.py
Normal file
@@ -0,0 +1,40 @@
|
||||
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)
|
45
bookmarks/tests/test_settings_export_view.py
Normal file
45
bookmarks/tests/test_settings_export_view.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class SettingsExportViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertFormErrorHint(self, response, text: str):
|
||||
self.assertContains(response, '<div class="has-error">')
|
||||
self.assertContains(response, text)
|
||||
|
||||
def test_should_export_successfully(self):
|
||||
self.setup_bookmark(tags=[self.setup_tag()])
|
||||
self.setup_bookmark(tags=[self.setup_tag()])
|
||||
self.setup_bookmark(tags=[self.setup_tag()])
|
||||
|
||||
response = self.client.get(
|
||||
reverse('bookmarks:settings.export'),
|
||||
follow=True
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['content-type'], 'text/plain; charset=UTF-8')
|
||||
self.assertEqual(response['Content-Disposition'], 'attachment; filename="bookmarks.html"')
|
||||
|
||||
def test_should_check_authentication(self):
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse('bookmarks:settings.export'), follow=True)
|
||||
|
||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.export'))
|
||||
|
||||
def test_should_show_hint_when_export_raises_error(self):
|
||||
with patch('bookmarks.services.exporter.export_netscape_html') as mock_export_netscape_html:
|
||||
mock_export_netscape_html.side_effect = Exception('Nope')
|
||||
response = self.client.get(reverse('bookmarks:settings.export'), follow=True)
|
||||
|
||||
self.assertTemplateUsed(response, 'settings/general.html')
|
||||
self.assertFormErrorHint(response, 'An error occurred during bookmark export.')
|
36
bookmarks/tests/test_settings_general_view.py
Normal file
36
bookmarks/tests/test_settings_general_view.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
from bookmarks.models import UserProfile
|
||||
|
||||
|
||||
class SettingsGeneralViewTestCase(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.general'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_should_check_authentication(self):
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse('bookmarks:settings.general'), follow=True)
|
||||
|
||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.general'))
|
||||
|
||||
def test_should_save_profile(self):
|
||||
form_data = {
|
||||
'theme': UserProfile.THEME_DARK,
|
||||
'bookmark_date_display': UserProfile.BOOKMARK_DATE_DISPLAY_HIDDEN,
|
||||
}
|
||||
response = self.client.post(reverse('bookmarks:settings.general'), form_data)
|
||||
|
||||
self.user.profile.refresh_from_db()
|
||||
|
||||
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'])
|
79
bookmarks/tests/test_settings_import_view.py
Normal file
79
bookmarks/tests/test_settings_import_view.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin, disable_logging
|
||||
|
||||
|
||||
class SettingsImportViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
def assertFormSuccessHint(self, response, text: str):
|
||||
self.assertContains(response, '<div class="has-success">')
|
||||
self.assertContains(response, text)
|
||||
|
||||
def assertNoFormSuccessHint(self, response):
|
||||
self.assertNotContains(response, '<div class="has-success">')
|
||||
|
||||
def assertFormErrorHint(self, response, text: str):
|
||||
self.assertContains(response, '<div class="has-error">')
|
||||
self.assertContains(response, text)
|
||||
|
||||
def assertNoFormErrorHint(self, response):
|
||||
self.assertNotContains(response, '<div class="has-error">')
|
||||
|
||||
def test_should_import_successfully(self):
|
||||
with open('bookmarks/tests/resources/simple_valid_import_file.html') as import_file:
|
||||
response = self.client.post(
|
||||
reverse('bookmarks:settings.import'),
|
||||
{'import_file': import_file},
|
||||
follow=True
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:settings.general'))
|
||||
self.assertFormSuccessHint(response, '3 bookmarks were successfully imported')
|
||||
self.assertNoFormErrorHint(response)
|
||||
|
||||
def test_should_check_authentication(self):
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse('bookmarks:settings.import'), follow=True)
|
||||
|
||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.import'))
|
||||
|
||||
def test_should_show_hint_if_there_is_no_file(self):
|
||||
response = self.client.post(
|
||||
reverse('bookmarks:settings.import'),
|
||||
follow=True
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:settings.general'))
|
||||
self.assertNoFormSuccessHint(response)
|
||||
self.assertFormErrorHint(response, 'Please select a file to import.')
|
||||
|
||||
@disable_logging
|
||||
def test_should_show_hint_if_import_raises_exception(self):
|
||||
with open('bookmarks/tests/resources/invalid_import_file.png', 'rb') as import_file:
|
||||
response = self.client.post(
|
||||
reverse('bookmarks:settings.import'),
|
||||
{'import_file': import_file},
|
||||
follow=True
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:settings.general'))
|
||||
self.assertNoFormSuccessHint(response)
|
||||
self.assertFormErrorHint(response, 'An error occurred during bookmark import.')
|
||||
|
||||
@disable_logging
|
||||
def test_should_show_respective_hints_if_not_all_bookmarks_were_imported_successfully(self):
|
||||
with open('bookmarks/tests/resources/simple_valid_import_file_with_one_invalid_bookmark.html') as import_file:
|
||||
response = self.client.post(
|
||||
reverse('bookmarks:settings.import'),
|
||||
{'import_file': import_file},
|
||||
follow=True
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse('bookmarks:settings.general'))
|
||||
self.assertFormSuccessHint(response, '2 bookmarks were successfully imported')
|
||||
self.assertFormErrorHint(response, '1 bookmarks could not be imported')
|
22
bookmarks/tests/test_settings_integrations_view.py
Normal file
22
bookmarks/tests/test_settings_integrations_view.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
|
||||
|
||||
class SettingsIntegrationsViewTestCase(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.integrations'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_should_check_authentication(self):
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse('bookmarks:settings.integrations'), follow=True)
|
||||
|
||||
self.assertRedirects(response, reverse('login') + '?next=' + reverse('bookmarks:settings.integrations'))
|
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from bookmarks.models import UserProfile
|
||||
|
||||
|
@@ -9,11 +9,17 @@ from rest_framework.authtoken.models import Token
|
||||
|
||||
from bookmarks.models import UserProfileForm
|
||||
from bookmarks.queries import query_bookmarks
|
||||
from bookmarks.services.exporter import export_netscape_html
|
||||
from bookmarks.services.importer import import_netscape_html
|
||||
from bookmarks.services import exporter
|
||||
from bookmarks.services import importer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
with open("version.txt", "r") as f:
|
||||
app_version = f.read().strip("\n")
|
||||
except Exception as exc:
|
||||
logging.exception(exc)
|
||||
pass
|
||||
|
||||
@login_required
|
||||
def general(request):
|
||||
@@ -30,6 +36,7 @@ def general(request):
|
||||
'form': form,
|
||||
'import_success_message': import_success_message,
|
||||
'import_errors_message': import_errors_message,
|
||||
'app_version': app_version
|
||||
})
|
||||
|
||||
|
||||
@@ -55,11 +62,11 @@ def bookmark_import(request):
|
||||
|
||||
if import_file is None:
|
||||
messages.error(request, 'Please select a file to import.', 'bookmark_import_errors')
|
||||
return HttpResponseRedirect(reverse('bookmarks:settings.index'))
|
||||
return HttpResponseRedirect(reverse('bookmarks:settings.general'))
|
||||
|
||||
try:
|
||||
content = import_file.read().decode()
|
||||
result = import_netscape_html(content, request.user)
|
||||
result = importer.import_netscape_html(content, request.user)
|
||||
success_msg = str(result.success) + ' bookmarks were successfully imported.'
|
||||
messages.success(request, success_msg, 'bookmark_import_success')
|
||||
if result.failed > 0:
|
||||
@@ -78,7 +85,7 @@ def bookmark_export(request):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
bookmarks = query_bookmarks(request.user, '')
|
||||
file_content = export_netscape_html(bookmarks)
|
||||
file_content = exporter.export_netscape_html(bookmarks)
|
||||
|
||||
response = HttpResponse(content_type='text/plain; charset=UTF-8')
|
||||
response['Content-Disposition'] = 'attachment; filename="bookmarks.html"'
|
||||
|
5
coverage.sh
Executable file
5
coverage.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
coverage erase
|
||||
coverage run manage.py test
|
||||
coverage report --sort=cover
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkding",
|
||||
"version": "1.4.0",
|
||||
"version": "1.7.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@@ -1,20 +1,22 @@
|
||||
asgiref==3.4.1
|
||||
beautifulsoup4==4.7.1
|
||||
certifi==2019.6.16
|
||||
chardet==3.0.4
|
||||
charset-normalizer==2.0.4
|
||||
confusable-homoglyphs==3.2.0
|
||||
Django==2.2.20
|
||||
Django==3.2.6
|
||||
django-generate-secret-key==1.0.2
|
||||
django-picklefield==2.0
|
||||
django-registration==3.1.2
|
||||
django-sass-processor==0.7.3
|
||||
django-widget-tweaks==1.4.5
|
||||
djangorestframework==3.11.2
|
||||
django-picklefield==3.0.1
|
||||
django-registration==3.2
|
||||
django-sass-processor==1.0.1
|
||||
django-widget-tweaks==1.4.8
|
||||
djangorestframework==3.12.4
|
||||
idna==2.8
|
||||
pyparsing==2.4.7
|
||||
python-dateutil==2.8.1
|
||||
pytz==2019.1
|
||||
requests==2.22.0
|
||||
pytz==2021.1
|
||||
requests==2.26.0
|
||||
soupsieve==1.9.2
|
||||
sqlparse==0.3.0
|
||||
urllib3==1.25.8
|
||||
sqlparse==0.4.1
|
||||
typing-extensions==3.10.0.0
|
||||
urllib3==1.26.6
|
||||
uWSGI==2.0.18
|
||||
|
@@ -1,26 +1,29 @@
|
||||
asgiref==3.4.1
|
||||
beautifulsoup4==4.7.1
|
||||
certifi==2019.6.16
|
||||
chardet==3.0.4
|
||||
charset-normalizer==2.0.4
|
||||
confusable-homoglyphs==3.2.0
|
||||
Django==2.2.20
|
||||
django-appconf==1.0.3
|
||||
django-compressor==2.3
|
||||
coverage==5.5
|
||||
Django==3.2.6
|
||||
django-appconf==1.0.4
|
||||
django-compressor==2.4.1
|
||||
django-debug-toolbar==3.2.1
|
||||
django-generate-secret-key==1.0.2
|
||||
django-picklefield==2.0
|
||||
django-registration==3.1.2
|
||||
django-sass-processor==0.7.3
|
||||
django-widget-tweaks==1.4.5
|
||||
djangorestframework==3.11.2
|
||||
django-picklefield==3.0.1
|
||||
django-registration==3.2
|
||||
django-sass-processor==1.0.1
|
||||
django-widget-tweaks==1.4.8
|
||||
djangorestframework==3.12.4
|
||||
idna==2.8
|
||||
libsass==0.19.2
|
||||
libsass==0.21.0
|
||||
pyparsing==2.4.7
|
||||
python-dateutil==2.8.1
|
||||
pytz==2019.1
|
||||
pytz==2021.1
|
||||
rcssmin==1.0.6
|
||||
requests==2.22.0
|
||||
requests==2.26.0
|
||||
rjsmin==1.1.0
|
||||
six==1.12.0
|
||||
six==1.16.0
|
||||
soupsieve==1.9.2
|
||||
sqlparse==0.3.0
|
||||
urllib3==1.25.8
|
||||
sqlparse==0.4.1
|
||||
typing-extensions==3.10.0.0
|
||||
urllib3==1.26.6
|
||||
|
@@ -73,6 +73,8 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
WSGI_APPLICATION = 'siteroot.wsgi.application'
|
||||
|
||||
# Database
|
||||
|
@@ -1 +1 @@
|
||||
1.6.4
|
||||
1.7.1
|
||||
|
Reference in New Issue
Block a user