diff --git a/.gitignore b/.gitignore index d547e5a9a..a6ba74d26 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ debian/plinth.preinst.debhelper debian/plinth.prerm.debhelper debian/plinth.substvars debian/plinth/ +*.pytest_cache/ \ No newline at end of file diff --git a/custom-shortcuts.json b/custom-shortcuts.json new file mode 100644 index 000000000..e69de29bb diff --git a/data/etc/plinth/custom-shortcuts.json b/data/etc/plinth/custom-shortcuts.json new file mode 100644 index 000000000..e69de29bb diff --git a/debian/control b/debian/control index 41a980ae6..3e7bb9c85 100644 --- a/debian/control +++ b/debian/control @@ -31,6 +31,7 @@ Build-Depends: debhelper (>= 11~) , python3-django-stronghold (>= 0.3.0) , python3-gi , python3-psutil + , python3-pytest , python3-requests , python3-ruamel.yaml , python3-setuptools diff --git a/plinth/modules/api/views.py b/plinth/modules/api/views.py index 0f230d784..4433ed0b1 100644 --- a/plinth/modules/api/views.py +++ b/plinth/modules/api/views.py @@ -20,12 +20,13 @@ FreedomBox app for api for android app. import copy import json +import os from django.core.serializers.json import DjangoJSONEncoder from django.http import HttpResponse from django.templatetags.static import static -from plinth import frontpage, module_loader +from plinth import cfg, frontpage, module_loader from plinth.modules import names @@ -44,14 +45,31 @@ def shortcuts(request, **kwargs): """API view to return the list of frontpage services.""" # XXX: Get the module (or module name) from shortcut properly. username = str(request.user) if request.user.is_authenticated else None + response = get_shortcuts_as_json(username) + return HttpResponse( + json.dumps(response, cls=DjangoJSONEncoder), + content_type='application/json') + + +def get_shortcuts_as_json(username=None): shortcuts = [ _get_shortcut_data(shortcut['id'].split('_')[0], shortcut) for shortcut in frontpage.get_shortcuts(username) ] - response = {'shortcuts': shortcuts} - return HttpResponse( - json.dumps(response, cls=DjangoJSONEncoder), - content_type='application/json') + custom_shortcuts = get_custom_shortcuts() + if custom_shortcuts: + shortcuts += custom_shortcuts['shortcuts'] + return {'shortcuts': shortcuts} + + +def get_custom_shortcuts(): + cfg_dir = os.path.dirname(cfg.config_file) + shortcuts_file = os.path.join(cfg_dir, 'custom-shortcuts.json') + if os.path.isfile(shortcuts_file) and os.stat(shortcuts_file).st_size: + with open(shortcuts_file) as shortcuts: + custom_shortcuts = json.load(shortcuts) + return custom_shortcuts + return None def _get_shortcut_data(module_name, shortcut): diff --git a/plinth/tests/data/etc/plinth/custom-shortcuts.json b/plinth/tests/data/etc/plinth/custom-shortcuts.json new file mode 100644 index 000000000..dde31ee29 --- /dev/null +++ b/plinth/tests/data/etc/plinth/custom-shortcuts.json @@ -0,0 +1 @@ +{"shortcuts": [{"name": "NextCloud", "short_description": "File Hosting Service", "description": "Nextcloud is a suite of client-server software for creating and using file hosting services.", "icon_url": "/plinth/custom/static/themes/default/icons/nextcloud.png", "clients": [{"name": "nextcloud", "platforms": [{"type": "web", "url": "/nextcloud"}]}]}]} \ No newline at end of file diff --git a/plinth/tests/data/etc/plinth/plinth.config b/plinth/tests/data/etc/plinth/plinth.config new file mode 100644 index 000000000..e428bd3f6 --- /dev/null +++ b/plinth/tests/data/etc/plinth/plinth.config @@ -0,0 +1,46 @@ +[Path] +# directory locations +file_root = %(root)s +config_dir = %(file_root)s/data/etc/plinth +data_dir = %(file_root)s/data/var/lib/plinth +log_dir = %(file_root)s/data/var/log/plinth +server_dir = /plinth +actions_dir = %(file_root)s/actions +doc_dir = %(file_root)s/doc +custom_static_dir = %(file_root)s/data/var/www/plinth/custom/static + +# file locations +store_file = %(data_dir)s/plinth.sqlite3 +status_log_file = %(log_dir)s/status.log +access_log_file = %(log_dir)s/access.log + +[Network] +host = 127.0.0.1 +port = 8000 + +# Enable the following only if Plinth is behind a proxy server. The +# proxy server should properly clean and the following HTTP headers: +# X-Forwarded-Host +# X-Forwarded-Proto +# If you enable these unnecessarily, this will lead to serious security +# problems. For more information, see +# https://docs.djangoproject.com/en/1.7/ref/settings/ +# +# These are enabled by default in Plinth because the default +# configuration allows only connections from localhost +# +# Leave the values blank to disable +use_x_forwarded_host = True +secure_proxy_ssl_header = HTTP_X_FORWARDED_PROTO + +[Misc] +box_name = FreedomBox +# The danube_edition changes the firstboot process and offers entering a +# voucher for a freedombox.me sub-domain. This functionality requires +# additional debian packages to be installed: +# +# pagekite, python3-requests +# +# They are not added as dependencies to keep the normal installation images +# lean, but make sure to add them if you want to build danube-edition images. +danube_edition = False diff --git a/plinth/tests/test_cfg.py b/plinth/tests/test_cfg.py index f81d8ddcf..4ac85db3a 100644 --- a/plinth/tests/test_cfg.py +++ b/plinth/tests/test_cfg.py @@ -115,10 +115,12 @@ class TestCfg(unittest.TestCase): """Compare two sets of configuration values.""" # Note that the count of items within each section includes the number # of default items (1, for 'root'). - self.assertEqual(11, len(parser.items('Path'))) + self.assertEqual(12, len(parser.items('Path'))) self.assertEqual(parser.get('Path', 'root'), cfg.root) self.assertEqual(parser.get('Path', 'file_root'), cfg.file_root) self.assertEqual(parser.get('Path', 'config_dir'), cfg.config_dir) + self.assertEqual( + parser.get('Path', 'custom_static_dir'), cfg.custom_static_dir) self.assertEqual(parser.get('Path', 'data_dir'), cfg.data_dir) self.assertEqual(parser.get('Path', 'store_file'), cfg.store_file) self.assertEqual(parser.get('Path', 'actions_dir'), cfg.actions_dir) diff --git a/plinth/tests/test_custom_shortcuts.py b/plinth/tests/test_custom_shortcuts.py new file mode 100644 index 000000000..be8e7240b --- /dev/null +++ b/plinth/tests/test_custom_shortcuts.py @@ -0,0 +1,123 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +""" +Test module for custom shortcuts. +""" + +import json +import os + +import pytest + +from plinth import cfg +from plinth.modules.api.views import get_shortcuts_as_json + +TEST_CONFIG_DIR = \ + os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data') + +CUSTOM_SHORTCUTS_FILE = os.path.join(TEST_CONFIG_DIR, + 'etc/plinth/custom-shortcuts.json') + +NEXTCLOUD_SHORTCUT = { + 'name': + 'NextCloud', + 'short_description': + 'File Hosting Service', + 'description': + 'Nextcloud is a suite of client-server software for creating ' + 'and using file hosting services.', + 'icon_url': + '/plinth/custom/static/themes/default/icons/nextcloud.png', + 'clients': [{ + 'name': 'nextcloud', + 'platforms': [{ + 'type': 'web', + 'url': '/nextcloud' + }] + }] +} + + +def setup_module(module): + """Load test configuration.""" + root = os.path.dirname(os.path.realpath(__file__)) + cfg_file = os.path.join(TEST_CONFIG_DIR, 'etc', 'plinth', 'plinth.config') + cfg.read(cfg_file, root) + + +def teardown_module(module): + """Reset configuration.""" + cfg.read() + + +@pytest.fixture +def no_custom_shortcuts_file(): + """Delete the custom_shortcuts file.""" + if os.path.exists(CUSTOM_SHORTCUTS_FILE): + os.remove(CUSTOM_SHORTCUTS_FILE) + + +@pytest.fixture +def blank_custom_shortcuts_file(): + """Create a blank shortcuts file.""" + open(CUSTOM_SHORTCUTS_FILE, 'w').close() + + +@pytest.fixture +def empty_custom_shortcuts(): + """Create a custom_shortcuts file with an empty list of shortcuts.""" + with open(CUSTOM_SHORTCUTS_FILE, 'w') as shortcuts_file: + shortcuts = {'shortcuts': []} + json.dump(shortcuts, shortcuts_file) + + +@pytest.fixture +def nextcloud_shortcut(): + with open(CUSTOM_SHORTCUTS_FILE, 'w') as shortcuts_file: + shortcuts = {'shortcuts': [NEXTCLOUD_SHORTCUT]} + json.dump(shortcuts, shortcuts_file) + + +def test_shortcuts_api_with_no_custom_shortcuts_file(no_custom_shortcuts_file): + get_shortcuts_as_json() + + +def test_shortcuts_api_with_blank_custom_shortcuts_file( + blank_custom_shortcuts_file): + get_shortcuts_as_json() + + +def test_shortcuts_api_with_empty_custom_shortcuts_list( + empty_custom_shortcuts): + get_shortcuts_as_json() + + +def test_shortcuts_api_with_custom_nextcloud_shortcut(nextcloud_shortcut): + shortcuts = get_shortcuts_as_json() + assert len(shortcuts['shortcuts']) >= 1 + assert any( + shortcut['name'] == 'NextCloud' for shortcut in shortcuts['shortcuts']) + + +def test_retrieved_custom_shortcut_from_api_is_correct(nextcloud_shortcut): + shortcuts = get_shortcuts_as_json() + nextcloud_shortcut = [ + shortcut for shortcut in shortcuts['shortcuts'] + if shortcut['name'] == 'NextCloud' + ] + assert nextcloud_shortcut + assert nextcloud_shortcut[0] == NEXTCLOUD_SHORTCUT diff --git a/setup.py b/setup.py index fc41fb086..0c557008c 100755 --- a/setup.py +++ b/setup.py @@ -252,7 +252,10 @@ setuptools.setup( ('/usr/share/polkit-1/rules.d', ['data/usr/share/polkit-1/rules.d/50-plinth.rules']), ('/usr/share/man/man1', ['doc/plinth.1']), - ('/etc/plinth', ['data/etc/plinth/plinth.config']), + ('/etc/plinth', [ + 'data/etc/plinth/plinth.config', + 'data/etc/plinth/custom-shortcuts.json' + ]), ('/usr/share/augeas/lenses', glob.glob('data/usr/share/augeas/lenses/*.aug')), ('/usr/share/augeas/lenses/tests', @@ -271,7 +274,7 @@ setuptools.setup( 'data/var/www/plinth/custom/static/themes/default/icons/.gitignore', # TODO Cannot be copied since a symlink is not a regular file # 'data/var/www/plinth/custom/static/theme', - ]) + ]), ], cmdclass={ 'install': CustomInstall,