diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 06047f1d..9dc9289d 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -33,6 +33,28 @@ ScenarioType = typing.Tuple[str, state.DataclassState] class View: + """ + A thing which exposes a ``.widget`` attribute which is a view on some + data. This view is essentially a complex combination of widgets, along with + some event handling capabilities, which may or may not be sent back up to + the underlying controller. + + We strive hard to keep "Model" data out of the View (and try to avoid + storing it at all on the View itself), instead relying on being able + to notify, and receive notifications, of important events from the Controller. + + """ + pass + + +class Controller: + """ + The singleton thing which is the top-level Application. + + It is responsible for owning the Model data and the Views, and + orchestrating event messages to each if the Model/View change. + + """ pass @@ -98,8 +120,13 @@ class ExposureModelResult(View): lines.append(f'Probability of infection: {np.round(P, 0)}%') lines.append(f'Number of exposed: {model.exposed.number}') - R0 = np.round(model.reproduction_rate(), 1) - lines.append(f'Number of expected new cases (R0): {R0}') + + new_cases = np.round(model.expected_new_cases(), 1) + lines.append(f'Number of expected new cases: {new_cases}') + + R0 = np.round(model.reproduction_number(), 1) + lines.append(f'Reproduction number (R0): {R0}') + self.html_output.value = '
\n'.join(lines) @@ -435,7 +462,7 @@ class CARAStateBuilder(state.StateBuilder): return s -class ExpertApplication: +class ExpertApplication(Controller): def __init__(self): self._debug_output = widgets.Output() @@ -479,7 +506,7 @@ class ExpertApplication: self._model_scenarios.append((name, model)) self._active_scenario = len(self._model_scenarios) - 1 model.dcs_observe(self.notify_model_values_changed) - self.notify_model_scenario_changed() + self.notify_scenarios_changed() def _find_model_id(self, model_id): for index, (name, model) in enumerate(list(self._model_scenarios)): @@ -491,22 +518,22 @@ class ExpertApplication: def rename_scenario(self, model_id, new_name): index, _, model = self._find_model_id(model_id) self._model_scenarios[index] = (new_name, model) - self.notify_model_scenario_changed() + self.notify_scenarios_changed() def remove_scenario(self, model_id): index, _, model = self._find_model_id(model_id) self._model_scenarios.pop(index) if self._active_scenario >= index: self._active_scenario = max(self._active_scenario - 1, 0) - self.notify_model_scenario_changed() + self.notify_scenarios_changed() def set_active_scenario(self, model_id): index, _, model = self._find_model_id(model_id) self._active_scenario = index - self.notify_model_scenario_changed() + self.notify_scenarios_changed() self.notify_model_values_changed() - def notify_model_scenario_changed(self): + def notify_scenarios_changed(self): """ Occurs when the set of scenarios has been modified, but not if the values of the scenario has changed. diff --git a/cara/tests/apps/test_expert_app.py b/cara/tests/apps/test_expert_app.py index 2edb5d28..3e4d2362 100644 --- a/cara/tests/apps/test_expert_app.py +++ b/cara/tests/apps/test_expert_app.py @@ -1,9 +1,22 @@ +import pytest + import cara.apps -def test_app(): +@pytest.fixture +def expert_app(): + return cara.apps.ExpertApplication() + + +def test_app(expert_app): # To start with, let's just test that the application runs. We don't try to # do anything fancy to verify how it looks etc., we leave that for manual # testing. - expert_app = cara.apps.ExpertApplication() - assert expert_app.multi_model_view.scenario_names[0] == "Scenario 1" + assert expert_app._model_scenarios[0][0] == "Scenario 1" + + +def test_new_scenario_changes_tab(expert_app): + # Adding a new scenario should change the tab index of the multi-model view. + assert expert_app.multi_model_view.widget.selected_index == 0 + expert_app.add_scenario("Another scenario") + assert expert_app.multi_model_view.widget.selected_index == 1