diff --git a/subiquity/models/keyboard.py b/subiquity/models/keyboard.py index b569608b..deefa5bb 100644 --- a/subiquity/models/keyboard.py +++ b/subiquity/models/keyboard.py @@ -20,7 +20,11 @@ import os import yaml from subiquity.common.resources import resource_path -from subiquity.common.types import KeyboardSetting +from subiquity.common.serialize import Serializer +from subiquity.common.types import ( + KeyboardLayout, + KeyboardSetting, +) log = logging.getLogger("subiquity.models.keyboard") @@ -71,6 +75,8 @@ class KeyboardModel: self.default_setting = from_config_file(self.config_path) else: self.default_setting = self.layout_for_lang['en_US.UTF-8'] + self.keyboard_list = KeyboardList() + self.keyboard_list.load_language('C') self._setting = None @property @@ -81,8 +87,18 @@ class KeyboardModel: @setting.setter def setting(self, value): + self.validate_setting(value) self._setting = value + def validate_setting(self, setting: KeyboardSetting) -> None: + kbd_layout = self.keyboard_list.layout_map.get(setting.layout) + if kbd_layout is None: + raise ValueError(f'Unknown keyboard layout "{setting.layout}"') + if not any(variant.code == setting.variant + for variant in kbd_layout.variants): + raise ValueError(f'Unknown keyboard variant "{setting.variant}" ' + f'for layout "{setting.layout}"') + def render_config_file(self): options = "" if self.setting.toggle: @@ -130,3 +146,44 @@ class KeyboardModel: for k, v in data.items(): ret[k] = KeyboardSetting(**v) return ret + + +class KeyboardList: + + def __init__(self): + self._kbnames_dir = resource_path('kbds') + self.serializer = Serializer(compact=True) + self._clear() + + def _file_for_lang(self, code): + return os.path.join(self._kbnames_dir, code + '.jsonl') + + def _has_language(self, code): + return os.path.exists(self._file_for_lang(code)) + + def load_language(self, code): + if '.' in code: + code = code.split('.')[0] + if not self._has_language(code): + code = code.split('_')[0] + if not self._has_language(code): + code = 'C' + + if code == self.current_lang: + return + + self._clear() + + with open(self._file_for_lang(code)) as kbdnames: + self.layouts = [] + self.layout_map = {} + for line in kbdnames: + kbd_layout = self.serializer.from_json(KeyboardLayout, line) + self.layouts.append(kbd_layout) + self.layout_map[kbd_layout.code] = kbd_layout + self.current_lang = code + + def _clear(self): + self.current_lang = None + self.layouts = [] + self.layout_map = {} diff --git a/subiquity/models/tests/test_keyboard.py b/subiquity/models/tests/test_keyboard.py index f1d482fb..5e20f465 100644 --- a/subiquity/models/tests/test_keyboard.py +++ b/subiquity/models/tests/test_keyboard.py @@ -29,11 +29,21 @@ class TestKeyboardModel(SubiTestCase): self.assertIsNone(self.model._setting) self.assertEqual('us', self.model.setting.layout) - def testSetToZZ(self): - val = KeyboardSetting(layout='zz') - self.model.setting = val - self.assertEqual(val, self.model.setting) - self.assertEqual(val, self.model._setting) + @parameterized.expand((['zz'], ['en'])) + def testSetToInvalidLayout(self, layout): + initial = self.model.setting + val = KeyboardSetting(layout=layout) + with self.assertRaises(ValueError): + self.model.setting = val + self.assertEqual(initial, self.model.setting) + + @parameterized.expand((['zz'])) + def testSetToInvalidVariant(self, variant): + initial = self.model.setting + val = KeyboardSetting(layout='us', variant=variant) + with self.assertRaises(ValueError): + self.model.setting = val + self.assertEqual(initial, self.model.setting) @parameterized.expand([ ['ast_ES.UTF-8', 'es', 'ast'], diff --git a/subiquity/server/controllers/keyboard.py b/subiquity/server/controllers/keyboard.py index 9ffa055a..b60fef55 100644 --- a/subiquity/server/controllers/keyboard.py +++ b/subiquity/server/controllers/keyboard.py @@ -28,7 +28,6 @@ from subiquity.common.resources import resource_path from subiquity.common.serialize import Serializer from subiquity.common.types import ( AnyStep, - KeyboardLayout, KeyboardSetting, KeyboardSetup, ) @@ -118,44 +117,6 @@ def for_ui(setting): layout=layout, variant=variant, toggle=setting.toggle) -class KeyboardList: - - def __init__(self): - self._kbnames_dir = resource_path('kbds') - self.serializer = Serializer(compact=True) - self._clear() - - def _file_for_lang(self, code): - return os.path.join(self._kbnames_dir, code + '.jsonl') - - def _has_language(self, code): - return os.path.exists(self._file_for_lang(code)) - - def load_language(self, code): - if '.' in code: - code = code.split('.')[0] - if not self._has_language(code): - code = code.split('_')[0] - if not self._has_language(code): - code = 'C' - - if code == self.current_lang: - return - - self._clear() - - with open(self._file_for_lang(code)) as kbdnames: - self.layouts = [ - self.serializer.from_json(KeyboardLayout, line) - for line in kbdnames - ] - self.current_lang = code - - def _clear(self): - self.current_lang = None - self.layouts = [] - - class KeyboardController(SubiquityController): endpoint = API.keyboard @@ -177,7 +138,6 @@ class KeyboardController(SubiquityController): self.serializer = Serializer(compact=True) self.pc105_steps = None self.needs_set_keyboard = False - self.keyboard_list = KeyboardList() super().__init__(app) def load_autoinstall_data(self, data): @@ -213,10 +173,10 @@ class KeyboardController(SubiquityController): async def GET(self) -> KeyboardSetup: lang = self.app.base_model.locale.selected_language - self.keyboard_list.load_language(lang) + self.model.keyboard_list.load_language(lang) return KeyboardSetup( setting=for_ui(self.model.setting_for_lang(lang)), - layouts=self.keyboard_list.layouts) + layouts=self.model.keyboard_list.layouts) async def POST(self, data: KeyboardSetting): log.debug(data)