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"'