diff --git a/plinth/app.py b/plinth/app.py index 0cc195039..45e6e0b94 100644 --- a/plinth/app.py +++ b/plinth/app.py @@ -4,6 +4,8 @@ Base class for all Freedombox applications. """ import collections +import enum +import sys from . import clients as clients_module @@ -40,6 +42,12 @@ class App: _all_apps = collections.OrderedDict() + class SetupState(enum.Enum): + """Various states of app being setup.""" + NEEDS_SETUP = 'needs-setup' + NEEDS_UPDATE = 'needs-update' + UP_TO_DATE = 'up-to-date' + def __init__(self): """Build the app by adding components. @@ -118,6 +126,51 @@ class App: for component in self.components.values(): component.setup(old_version=old_version) + def get_setup_state(self) -> SetupState: + """Return whether the app is not setup or needs upgrade.""" + current_version = self.get_setup_version() + if current_version and self.info.version <= current_version: + return self.SetupState.UP_TO_DATE + + # If an app needs installing/updating but no setup method is available, + # then automatically set version. + # + # Minor violation of 'get' only discipline for convenience. + module = sys.modules[self.__module__] + if not hasattr(module, 'setup'): + self.set_setup_version(self.info.version) + return self.SetupState.UP_TO_DATE + + if not current_version: + return self.SetupState.NEEDS_SETUP + + return self.SetupState.NEEDS_UPDATE + + def get_setup_version(self) -> int: + """Return the setup version of the app.""" + # XXX: Optimize version gets + from . import models + + try: + app_entry = models.Module.objects.get(pk=self.app_id) + return app_entry.setup_version + except models.Module.DoesNotExist: + return 0 + + def needs_setup(self) -> bool: + """Return whether the app needs to be setup. + + A simple shortcut for get_setup_state() == NEEDS_SETUP + """ + return self.get_setup_state() == self.SetupState.NEEDS_SETUP + + def set_setup_version(self, version: int) -> None: + """Set the app's setup version.""" + from . import models + + models.Module.objects.update_or_create( + pk=self.app_id, defaults={'setup_version': version}) + def enable(self): """Enable all the components of the app.""" for component in self.components.values(): diff --git a/plinth/tests/test_app.py b/plinth/tests/test_app.py index 95e95b092..759b904f0 100644 --- a/plinth/tests/test_app.py +++ b/plinth/tests/test_app.py @@ -4,6 +4,7 @@ Test module for App, base class for all applications. """ import collections +import sys from unittest.mock import Mock, call, patch import pytest @@ -18,6 +19,16 @@ class AppTest(App): app_id = 'test-app' +class AppSetupTest(App): + """Sample App for testing setup operations.""" + app_id = 'test-app-setup' + + def __init__(self): + super().__init__() + info = Info('test-app-setup', 3) + self.add(info) + + class LeaderTest(FollowerComponent): """Test class for using LeaderComponent in tests.""" is_leader = True @@ -124,6 +135,52 @@ def test_app_setup(app_with_components): component.setup.assert_has_calls([call(old_version=2)]) +@pytest.mark.django_db +def test_setup_state(): + """Test retrieving the current setup state of the app.""" + app = AppSetupTest() + app.info.version = 3 + module = sys.modules[__name__] + + app.set_setup_version(3) + assert app.get_setup_state() == App.SetupState.UP_TO_DATE + assert not app.needs_setup() + + app.set_setup_version(0) + try: + delattr(module, 'setup') + except AttributeError: + pass + assert app.get_setup_state() == App.SetupState.UP_TO_DATE + assert not app.needs_setup() + assert app.get_setup_version() == 3 + + setattr(module, 'setup', True) + app.set_setup_version(0) + assert app.get_setup_state() == App.SetupState.NEEDS_SETUP + assert app.needs_setup() + + app.set_setup_version(2) + assert app.get_setup_state() == App.SetupState.NEEDS_UPDATE + assert not app.needs_setup() + + +@pytest.mark.django_db +def test_get_set_setup_version(): + """Setting and getting the setup version of the app.""" + app = AppSetupTest() + + from plinth import models + try: + models.Module.objects.get(pk=app.app_id).delete() + except models.Module.DoesNotExist: + pass + assert app.get_setup_version() == 0 + + app.set_setup_version(5) + assert app.get_setup_version() == 5 + + def test_app_enable(app_with_components): """Test that enabling an app enables components.""" app_with_components.disable()