diff --git a/bookmarks/services/importer.py b/bookmarks/services/importer.py
index 6dfe949..a315019 100644
--- a/bookmarks/services/importer.py
+++ b/bookmarks/services/importer.py
@@ -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
diff --git a/bookmarks/services/parser.py b/bookmarks/services/parser.py
index 39079a4..ed134da 100644
--- a/bookmarks/services/parser.py
+++ b/bookmarks/services/parser.py
@@ -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,
diff --git a/bookmarks/tests/resources/invalid_import_file.png b/bookmarks/tests/resources/invalid_import_file.png
new file mode 100644
index 0000000..f5c32c2
Binary files /dev/null and b/bookmarks/tests/resources/invalid_import_file.png differ
diff --git a/bookmarks/tests/resources/simple_valid_import_file.html b/bookmarks/tests/resources/simple_valid_import_file.html
new file mode 100644
index 0000000..74435aa
--- /dev/null
+++ b/bookmarks/tests/resources/simple_valid_import_file.html
@@ -0,0 +1,20 @@
+
+
+
+
+
Bookmarks
+
+Bookmarks
+
+
+
+
- test title 1
+
- test description 1
+
+
- test title 2
+
- test description 2
+
+
- test title 3
+
- test description 3
+
+
\ No newline at end of file
diff --git a/bookmarks/tests/resources/simple_valid_import_file_with_one_invalid_bookmark.html b/bookmarks/tests/resources/simple_valid_import_file_with_one_invalid_bookmark.html
new file mode 100644
index 0000000..f3eec2f
--- /dev/null
+++ b/bookmarks/tests/resources/simple_valid_import_file_with_one_invalid_bookmark.html
@@ -0,0 +1,20 @@
+
+
+
+
+
Bookmarks
+
+Bookmarks
+
+
+
+
- test title 1
+
- test description 1
+
+
- test title 2
+
- test description 2
+
+
- test title 3
+
- test description 3
+
+
\ No newline at end of file
diff --git a/bookmarks/tests/test_settings_api_view.py b/bookmarks/tests/test_settings_api_view.py
new file mode 100644
index 0000000..587728b
--- /dev/null
+++ b/bookmarks/tests/test_settings_api_view.py
@@ -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)
diff --git a/bookmarks/tests/test_settings_export_view.py b/bookmarks/tests/test_settings_export_view.py
new file mode 100644
index 0000000..e1d56b1
--- /dev/null
+++ b/bookmarks/tests/test_settings_export_view.py
@@ -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, '
')
+ 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.')
diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py
new file mode 100644
index 0000000..642c0c0
--- /dev/null
+++ b/bookmarks/tests/test_settings_general_view.py
@@ -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'])
diff --git a/bookmarks/tests/test_settings_import_view.py b/bookmarks/tests/test_settings_import_view.py
new file mode 100644
index 0000000..b5c609a
--- /dev/null
+++ b/bookmarks/tests/test_settings_import_view.py
@@ -0,0 +1,77 @@
+from django.test import TestCase
+from django.urls import reverse
+
+from bookmarks.tests.helpers import BookmarkFactoryMixin
+
+
+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, '
')
+ self.assertContains(response, text)
+
+ def assertNoFormSuccessHint(self, response):
+ self.assertNotContains(response, '
')
+
+ def assertFormErrorHint(self, response, text: str):
+ self.assertContains(response, '
')
+ self.assertContains(response, text)
+
+ def assertNoFormErrorHint(self, response):
+ self.assertNotContains(response, '
')
+
+ 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.')
+
+ 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.')
+
+ 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')
diff --git a/bookmarks/tests/test_settings_integrations_view.py b/bookmarks/tests/test_settings_integrations_view.py
new file mode 100644
index 0000000..914b79f
--- /dev/null
+++ b/bookmarks/tests/test_settings_integrations_view.py
@@ -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'))
diff --git a/bookmarks/views/settings.py b/bookmarks/views/settings.py
index d3d9b17..da97e23 100644
--- a/bookmarks/views/settings.py
+++ b/bookmarks/views/settings.py
@@ -9,8 +9,8 @@ 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__)
@@ -55,11 +55,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 +78,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"'