From acd248e5063aedb5fb07c8189d38ae59039aecc1 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 6 Dec 2017 15:20:57 +0530 Subject: [PATCH] clients: Cleanup framework - Move all utilities to a separate clients.py module. Tests too. - Use fewer custom template tags. Actually only one tag is really required. Keeping custom tags minimal is a goal. - Merge the methods to generate app store URLs. - Implement a validator for validating client information and use that instead of enums. - Internationalize the text on template page. - Add missing RPM package case. - Cleanup CSS. Remove unused styles, minimize the styles set. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/clients.py | 146 +++++++++++++++++++ plinth/templates/clients.html | 203 ++++++++++++++------------- plinth/templatetags/plinth_extras.py | 88 +----------- plinth/tests/test_clients.py | 52 +++++++ plinth/tests/test_templatetags.py | 23 --- plinth/utils.py | 9 -- static/themes/default/css/plinth.css | 59 ++------ 7 files changed, 325 insertions(+), 255 deletions(-) create mode 100644 plinth/clients.py create mode 100644 plinth/tests/test_clients.py diff --git a/plinth/clients.py b/plinth/clients.py new file mode 100644 index 000000000..b00a4d55d --- /dev/null +++ b/plinth/clients.py @@ -0,0 +1,146 @@ +# +# This file is part of Plinth. +# +# 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 . +# +""" +Utility methods for providing client information. +""" + +from django.utils.functional import Promise +from enum import Enum + + +class Desktop_OS(Enum): + GNU_LINUX = 'gnu-linux' + MAC_OS = 'macos' + WINDOWS = 'windows' + + +class Mobile_OS(Enum): + ANDROID = 'android' + IOS = 'ios' + + +class Store(Enum): + APP_STORE = 'app-store' + F_DROID = 'f-droid' + GOOGLE_PLAY = 'google-play' + + +class Package(Enum): + DEB = 'deb' + HOMEBREW = 'brew' + RPM = 'rpm' + + +def enum_values(enum): + return [option.value for option in list(enum)] + + +def _check(client, condition): + """Check if any of a list of clients satisfies the given condition""" + return any(platform for platform in client['platforms'] + if condition(platform)) + + +def _client_has_desktop(client): + """Filter to find out whether an application has desktop clients""" + return _check( + client, lambda platform: platform.get('os') in enum_values(Desktop_OS)) + + +def _client_has_mobile(client): + """Filter to find out whether an application has mobile clients""" + return _check( + client, lambda platform: platform.get('os') in enum_values(Mobile_OS)) + + +def _client_has_web(client): + """Filter to find out whether an application has web clients""" + return _check(client, lambda platform: platform['type'] == 'web') + + +def _client_has_package(client): + """Filter to find out whether an application has web clients""" + return _check(client, lambda platform: platform['type'] == 'package') + + +def of_type(clients, client_type): + """Filter and get clients of a particular type""" + filters = { + 'mobile': _client_has_mobile, + 'desktop': _client_has_desktop, + 'web': _client_has_web, + 'package': _client_has_package, + } + return list(filter(filters[client_type], clients)) + + +def store_url(store, package_id): + """Return a full App store URL given package id and type of store.""" + stores = { + 'google-play': 'https://play.google.com/store/apps/details?id={}', + 'f-droid': 'https://f-droid.org/packages/{}' + } + return stores[store].format(package_id) + + +def validate(clients): + """Validate the clients' information schema.""" + assert isinstance(clients, list) + for client in clients: + _validate_client(client) + + return clients + + +def _validate_client(client): + """Validate a single client's information schema.""" + assert isinstance(client, dict) + assert 'name' in client + assert isinstance(client['platforms'], list) + for platform in client['platforms']: + _validate_platform(platform) + + +def _validate_platform(platform): + """Validate a single platform's schema.""" + assert platform['type'] in ('package', 'download', 'store', 'web') + validate_method = globals()['_validate_platform_' + platform['type']] + validate_method(platform) + + +def _validate_platform_package(platform): + """Validate a platform of type package.""" + assert platform['format'] in enum_values(Package) + assert isinstance(platform['name'], (str, Promise)) + + +def _validate_platform_download(platform): + """Validate a platform of type download.""" + assert platform['os'] in enum_values(Desktop_OS) + assert isinstance(platform['url'], (str, Promise)) + + +def _validate_platform_store(platform): + """Validate a platform of type store.""" + assert platform['os'] in enum_values(Mobile_OS) + assert platform['store_name'] in enum_values(Store) + assert isinstance(platform['url'], (str, Promise)) + + +def _validate_platform_web(platform): + """Validate a platform of type web.""" + assert isinstance(platform['url'], (str, Promise)) diff --git a/plinth/templates/clients.html b/plinth/templates/clients.html index 0e9158d7b..81a13c0d8 100644 --- a/plinth/templates/clients.html +++ b/plinth/templates/clients.html @@ -23,113 +23,126 @@ {% if clients %}

-

- +
+
- {% if clients|has_web_clients %} - {% with clients|of_type:'web' as web_clients %} - {% for client in web_clients %} - - {% if forloop.counter == 1 %} - + {% with clients|clients_of_type:'web' as web_clients %} + {% for client in web_clients %} + + {% if forloop.counter == 1 %} + + {% endif %} + {% for platform in client.platforms %} + {% if platform.type == 'web' %} + + {% endif %} + {% endfor %} + + {% endfor %} + {% endwith %} + + {% with clients|clients_of_type:'desktop' as desktop_clients %} + {% for client in desktop_clients %} + + {% if forloop.counter == 1 %} + + {% endif %} + + - + {% if platform.type == 'download' %} + + {% with 'theme/icons/'|add:platform.os|add:'.png' as icon %} + + {% if platform.os == 'gnu-linux' %} + {% trans 'Play Store' %} + {% elif platform.os == 'windows' %} + {% trans 'Windows' %} + {% elif platform.os == 'macos' %} + {% trans 'macOS' %} + {% endif %} + {% endwith %} + {% endif %} {% endfor %} - - {% endfor %} - {% endwith %} - {% endif %} + + + {% endfor %} + {% endwith %} - {% if clients|has_desktop_clients %} - {% with clients|of_type:'desktop' as desktop_clients %} - {% for client in desktop_clients %} - - {% if forloop.counter == 1 %} - - {% endif %} - - - - {% endfor %} - {% endwith %} - {% endif %} - - {% if clients|has_mobile_clients %} - {% with clients|of_type:'mobile' as mobile_clients %} - {% for client in mobile_clients %} - - {% if forloop.counter == 1 %} - - {% endif %} - - - - {% endfor %} - {% endwith %} - {% endif %} - - {% if clients|has_package_clients %} - {% with clients|of_type:'package' as package_clients %} - {% for client in package_clients %} - - {% if forloop.counter == 1 %} - - {% endif %} - - + {% if forloop.counter == 1 %} + + {% endif %} + + - - {% endfor %} - {% endwith %} - {% endif %} + {% endwith %} + + {% endif %} + {% endfor %} + + + {% endfor %} + {% endwith %} + + {% with clients|clients_of_type:'package' as package_clients %} + {% for client in package_clients %} + + {% if forloop.counter == 1 %} + + {% endif %} + + + + {% endfor %} + {% endwith %}
Web
{% trans "Web" %}{{ client.name }} + + {% trans "Launch" %} + + +
{% trans "Desktop" %}{{ client.name }} {% for platform in client.platforms %} - {% if platform.type == 'web' %} - {{ client.name }} - - Launch - -
Desktop {{ client.name }} - {% for platform in client.platforms %} - {% if platform.type == 'download' %} - - {% with 'theme/icons/'|add:platform.os|add:'.png' as icon %} - {{ platform.os|display_name }} - {% endwith %} - - {% endif %} - {% endfor %} -
Mobile {{ client.name }} - {% for platform in client.platforms %} - {% if platform.type == 'store' and platform.os == 'android' or platform.os == 'ios' %} - - {% with 'theme/icons/'|add:platform.store_name|add:'.png' as icon %} - {{ platform.store_name|display_name }} - {% endwith %} - - {% endif %} - {% endfor %} -
Package {{ client.name }} -
-
    - {% for platform in client.platforms %} - {% if platform.type == 'package' %} - {% if platform.type == 'package' and platform.format == 'deb' %} -
  • Debian: {{ platform.name }}
  • - {% endif %} - {% if platform.type == 'package' and platform.format == 'homebrew' %} -
  • HomeBrew: {{ platform.name }}
  • - {% endif %} + {% with clients|clients_of_type:'mobile' as mobile_clients %} + {% for client in mobile_clients %} +
{% trans "Mobile" %}{{ client.name }} + {% for platform in client.platforms %} + {% if platform.type == 'store' and platform.os == 'android' or platform.os == 'ios' %} + + {% with 'theme/icons/'|add:platform.store_name|add:'.png' as icon %} + + {% if platform.store_name == 'google-play' %} + {% trans 'Play Store' %} + {% elif platform.store_name == 'f-droid' %} + {% trans 'F-Droid' %} + {% elif platform.store_name == 'app-store' %} + {% trans 'App Store' %} {% endif %} - {% endfor %} - - -
{% trans "Package" %}{{ client.name }} +
+
    + {% for platform in client.platforms %} + {% if platform.type == 'package' %} + {% if platform.format == 'deb' %} +
  • {% trans "Debian:" %} {{ platform.name }}
  • + {% endif %} + {% if platform.format == 'homebrew' %} +
  • {% trans "Homebrew:" %} {{ platform.name }}
  • + {% endif %} + {% if platform.format == 'rpm' %} +
  • {% trans "RPM:" %} {{ platform.name }}
  • + {% endif %} + {% endif %} + {% endfor %} +
+
+
+ {% endif %} diff --git a/plinth/templatetags/plinth_extras.py b/plinth/templatetags/plinth_extras.py index c62ae89de..d9e5f58d1 100644 --- a/plinth/templatetags/plinth_extras.py +++ b/plinth/templatetags/plinth_extras.py @@ -16,40 +16,14 @@ # import os -from enum import Enum from django import template +from plinth import clients as clients_module + register = template.Library() -class Desktop_OS(Enum): - GNU_LINUX = 'gnu-linux' - MAC_OS = 'mac-os' - WINDOWS = 'windows' - - -class Mobile_OS(Enum): - ANDROID = 'android' - IOS = 'ios' - - -class Store(Enum): - APP_STORE = 'app-store' - F_DROID = 'f-droid' - GOOGLE_PLAY = 'google-play' - - -class Package(Enum): - DEB = 'deb' - HOMEBREW = 'brew' - RPM = 'rpm' - - -def enum_values(enum): - return [x.value for x in list(enum)] - - def mark_active_menuitem(menu, path): """Mark the best-matching menu item with 'active' @@ -89,59 +63,7 @@ def show_subsubmenu(context, menu): return {'subsubmenu': menu} -def __check(clients, cond): - """Check if any of a list of clients satisfies the given condition""" - clients = clients if isinstance(clients, list) else [clients] - return any(pf for client in clients for pf in client['platforms'] - if cond(pf)) - - -@register.filter(name='has_desktop_clients') -def has_desktop_clients(clients): - """Filter to find out whether an application has desktop clients""" - return __check(clients, - lambda x: x.get('os', '') in enum_values(Desktop_OS)) - - -@register.filter(name='has_mobile_clients') -def has_mobile_clients(clients): - """Filter to find out whether an application has mobile clients""" - return __check(clients, - lambda x: x.get('os', '') in enum_values(Mobile_OS)) - - -@register.filter(name='has_web_clients') -def has_web_clients(clients): - """Filter to find out whether an application has web clients""" - return __check(clients, lambda x: x['type'] == 'web') - - -@register.filter(name='has_package_clients') -def has_package_clients(clients): - """Filter to find out whether an application has web clients""" - return __check(clients, lambda x: x['type'] == 'package') - - -@register.filter(name='of_type') -def of_type(clients, typ): +@register.filter(name='clients_of_type') +def clients_of_type(clients, client_type): """Filter and get clients of a particular type""" - filters = { - 'mobile': has_mobile_clients, - 'desktop': has_desktop_clients, - 'web': has_web_clients, - 'package': has_package_clients, - } - return list(filter(filters.get(typ, lambda x: x), clients)) - - -@register.filter(name='display_name') -def display_name(string): - names = { - 'gnu-linux': 'GNU/Linux', - 'windows': 'Windows', - 'mac-os': 'macOS', - 'google-play': 'Play Store', - 'f-droid': 'F-Droid', - 'app-store': 'App Store' - } - return names.get(string, string) + return clients_module.of_type(clients, client_type) diff --git a/plinth/tests/test_clients.py b/plinth/tests/test_clients.py new file mode 100644 index 000000000..ca1d16c22 --- /dev/null +++ b/plinth/tests/test_clients.py @@ -0,0 +1,52 @@ +# +# This file is part of Plinth. +# +# 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 clients module. +""" + +import unittest + +from plinth import clients +from plinth.modules.deluge.manifest import clients as deluge_clients +from plinth.modules.infinoted.manifest import clients as infinoted_clients +from plinth.modules.quassel.manifest import clients as quassel_clients +from plinth.modules.syncthing.manifest import clients as syncthing_clients +from plinth.modules.tor.manifest import clients as tor_clients + + +class TestClients(unittest.TestCase): + """Test utilities provided by clients module.""" + + def test_of_type_web(self): + """Test filtering clients of type web.""" + self.assertTrue(clients.of_type(syncthing_clients, 'web')) + self.assertFalse(clients.of_type(quassel_clients, 'web')) + + def test_of_type_mobile(self): + """Test filtering clients of type mobile.""" + self.assertTrue(clients.of_type(syncthing_clients, 'mobile')) + self.assertFalse(clients.of_type(infinoted_clients, 'mobile')) + + def test_of_type_desktop(self): + """Test filtering clients of type desktop.""" + self.assertTrue(clients.of_type(syncthing_clients, 'desktop')) + self.assertFalse(clients.of_type(deluge_clients, 'desktop')) + + def test_of_type_package(self): + """Test filtering clients of type package.""" + self.assertTrue(clients.of_type(syncthing_clients, 'package')) + self.assertFalse(clients.of_type(tor_clients, 'package')) diff --git a/plinth/tests/test_templatetags.py b/plinth/tests/test_templatetags.py index f47cfb9a3..819b71f8a 100644 --- a/plinth/tests/test_templatetags.py +++ b/plinth/tests/test_templatetags.py @@ -20,10 +20,6 @@ Test module for custom Django template tags. import unittest -from plinth.modules.syncthing.manifest import clients as syncthing_clients -from plinth.modules.infinoted.manifest import clients as infinoted_clients -from plinth.modules.deluge.manifest import clients as deluge_clients -from plinth.modules.quassel.manifest import clients as quassel_clients from plinth.templatetags import plinth_extras @@ -68,22 +64,3 @@ class TestShowSubSubMenu(unittest.TestCase): menu = plinth_extras.mark_active_menuitem(menu, check_path) self.assert_active_url(menu, expected_active_path) self.assertTrue(self._verify_active_menuitems(menu)) - - def test_has_web_clients(self): - """Test for a utility function that returns - whether an application has web clients""" - self.assertTrue(plinth_extras.has_web_clients(syncthing_clients)) - self.assertFalse(plinth_extras.has_web_clients(quassel_clients)) - - def test_has_mobile_clients(self): - """Test for a utility function that returns - whether an application has mobile clients""" - self.assertTrue(plinth_extras.has_mobile_clients(syncthing_clients)) - self.assertFalse(plinth_extras.has_mobile_clients(infinoted_clients)) - - def test_has_desktop_clients(self): - """Test for a utility function that returns - whether an application has desktop clients""" - self.assertTrue( - plinth_extras.has_desktop_clients(syncthing_clients[0])) - self.assertFalse(plinth_extras.has_desktop_clients(deluge_clients)) diff --git a/plinth/utils.py b/plinth/utils.py index 20ccfe6ac..bdd32e1e2 100644 --- a/plinth/utils.py +++ b/plinth/utils.py @@ -115,12 +115,3 @@ class YAMLFile(object): def is_file_empty(self): return os.stat(self.yaml_file).st_size == 0 - - -def play_store_url(package_id): - return 'https://play.google.com/store/apps/details?id={}'.format( - package_id) - - -def f_droid_url(package_id): - return 'https://f-droid.org/packages/{}'.format(package_id) diff --git a/static/themes/default/css/plinth.css b/static/themes/default/css/plinth.css index a2aeb5583..6894d6e22 100644 --- a/static/themes/default/css/plinth.css +++ b/static/themes/default/css/plinth.css @@ -43,10 +43,8 @@ body { } .running-status.loading { - border: 4px solid #f3f3f3; - /* Light grey */ - border-top: 4px solid #3498db; - /* Blue */ + border: 4px solid #f3f3f3; /* Light grey */ + border-top: 4px solid #3498db; /* Blue */ border-radius: 50%; width: 16px; height: 16px; @@ -68,13 +66,11 @@ body { } /* Hide log out button if user dropdown is available */ - .js #logout-nojs { display: none; } /* Hide the dropdown icon when javascript is not available */ - .no-js .nav .dropdown .caret { display: none; } @@ -85,7 +81,6 @@ body { /* Sticky footer styles -------------------------------------------------- */ - footer .license-info { opacity: 0.75; } @@ -125,25 +120,13 @@ footer license-info p { margin: 20px 0; } -.clients-info { - padding-top: 15px; -} - -.heading { - font-weight: bold; - display: inline; -} - -.clients-info li { - padding-left: 15px; - padding-bottom: 5px; -} - -.clients-info li span { - position: relative; - left: -18px; +.shortcut-label { + min-height: 50px; } +/* + * Clients information + */ .client-icon { display: inline-block; width: 100%; @@ -162,20 +145,17 @@ footer license-info p { line-height: 3.1em; } -.shortcut-label { - min-height: 50px; -} - - /* Icon when collapsible content is shown */ -#collapsible-button:after { - font-family: "Glyphicons Halflings"; - content: "\e114"; +#clients-button .glyphicon { margin-left: 5px; } -/* Icon when collapsible content is hidden */ -#collapsible-button.collapsed:after { +#clients-button .glyphicon:before, +.no-js #clients-button.collapsed .glyphicon:before { + content: "\e114"; +} + +#clients-button.collapsed .glyphicon:before { content: "\e080"; } @@ -183,14 +163,3 @@ footer license-info p { .no-js .collapse { display: block; } - -.no-js #collapsible-button:after { - font-family: "Glyphicons Halflings"; - content: "\e114"; - margin-left: 5px; -} - -.no-js #collapsible-button.collapsed:after { - content: "\e114"; -} -