diff --git a/bookmarks/styles/settings.scss b/bookmarks/styles/settings.scss index b60bb96..0940f0d 100644 --- a/bookmarks/styles/settings.scss +++ b/bookmarks/styles/settings.scss @@ -11,4 +11,8 @@ .input-group > input[type=submit] { height: auto; } + + section.about table { + max-width: 500px; + } } diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html index 4cec26b..06bd358 100644 --- a/bookmarks/templates/settings/general.html +++ b/bookmarks/templates/settings/general.html @@ -98,13 +98,29 @@ {# About section #} -
+

About

-

Version: {{ app_version }}

-

- Code: GitHub -

+ + + + + + + + + + + + + + + + + +
Version{{ version_info }}
LinksGitHub
Documentation
Changelog
diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py index 2278f82..08ff2c1 100644 --- a/bookmarks/tests/test_settings_general_view.py +++ b/bookmarks/tests/test_settings_general_view.py @@ -1,8 +1,14 @@ +import random + from django.test import TestCase from django.urls import reverse +from unittest.mock import patch, Mock +import requests +from requests import RequestException -from bookmarks.tests.helpers import BookmarkFactoryMixin from bookmarks.models import UserProfile +from bookmarks.tests.helpers import BookmarkFactoryMixin +from bookmarks.views.settings import app_version, get_version_info class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin): @@ -38,3 +44,31 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin): self.assertEqual(self.user.profile.bookmark_date_display, form_data['bookmark_date_display']) self.assertEqual(self.user.profile.bookmark_link_target, form_data['bookmark_link_target']) self.assertEqual(self.user.profile.web_archive_integration, form_data['web_archive_integration']) + + def test_about_shows_version_info(self): + response = self.client.get(reverse('bookmarks:settings.general')) + html = response.content.decode() + + self.assertInHTML(f''' + + Version + {get_version_info(random.random())} + + ''', html) + + def test_get_version_info_just_displays_latest_when_versions_are_equal(self): + latest_version_response_mock = Mock(status_code=201, json=lambda: {'name': f'v{app_version}'}) + with patch.object(requests, 'get', return_value=latest_version_response_mock): + version_info = get_version_info(random.random()) + self.assertEqual(version_info, f'{app_version} (latest)') + + def test_get_version_info_shows_latest_version_when_versions_are_not_equal(self): + latest_version_response_mock = Mock(status_code=201, json=lambda: {'name': f'v123.0.1'}) + with patch.object(requests, 'get', return_value=latest_version_response_mock): + version_info = get_version_info(random.random()) + self.assertEqual(version_info, f'{app_version} (latest: 123.0.1)') + + def test_get_version_info_silently_ignores_request_errors(self): + with patch.object(requests, 'get', side_effect=RequestException()): + version_info = get_version_info(random.random()) + self.assertEqual(version_info, f'{app_version}') diff --git a/bookmarks/views/settings.py b/bookmarks/views/settings.py index 1f9b536..e5dd19c 100644 --- a/bookmarks/views/settings.py +++ b/bookmarks/views/settings.py @@ -1,5 +1,8 @@ import logging +import time +from functools import lru_cache +import requests from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import HttpResponseRedirect, HttpResponse @@ -17,10 +20,11 @@ logger = logging.getLogger(__name__) try: with open("version.txt", "r") as f: app_version = f.read().strip("\n") -except Exception as exc: +except Exception as exc: logging.exception(exc) pass + @login_required def general(request): if request.method == 'POST': @@ -32,14 +36,41 @@ def general(request): import_success_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_success') import_errors_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_errors') + version_info = get_version_info(get_ttl_hash()) return render(request, 'settings/general.html', { 'form': form, 'import_success_message': import_success_message, 'import_errors_message': import_errors_message, - 'app_version': app_version + 'version_info': version_info, }) +# Cache API call response, for one hour when using get_ttl_hash with default params +@lru_cache(maxsize=1) +def get_version_info(ttl_hash=None): + latest_version = None + try: + latest_version_url = 'https://api.github.com/repos/sissbruecker/linkding/releases/latest' + response = requests.get(latest_version_url, timeout=5) + json = response.json() + latest_version = json['name'][1:] + except requests.exceptions.RequestException: + pass + + latest_version_info = '' + if latest_version == app_version: + latest_version_info = ' (latest)' + elif latest_version is not None: + latest_version_info = f' (latest: {latest_version})' + + return f'{app_version}{latest_version_info}' + + +def get_ttl_hash(seconds=3600): + """Return the same value within `seconds` time period""" + return round(time.time() / seconds) + + @login_required def integrations(request): application_url = request.build_absolute_uri("/bookmarks/new")