• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Open-MSS / MSS / 20679295966

03 Jan 2026 03:33PM UTC coverage: 70.014% (-0.04%) from 70.054%
20679295966

push

github

web-flow
update for deprecated Query.get() (#2960)

* update for deprecated Query.get()

* fix call of send_from_directory

3 of 12 new or added lines in 2 files covered. (25.0%)

419 existing lines in 12 files now uncovered.

14439 of 20623 relevant lines covered (70.01%)

0.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.3
/mslib/msui/editor.py
1
# -*- coding: utf-8 -*-
2
"""
3

4
    mslib.msui.editor
5
    ~~~~~~~~~~~~~~~~~~~~~~
6

7
    config editor for msui_settings.json.
8

9
    This file is part of MSS.
10

11
    :copyright: Copyright 2020 Vaibhav Mehra <veb7vmehra@gmail.com>
12
    :copyright: Copyright 2020-2025 by the MSS team, see AUTHORS.
13
    :license: APACHE-2.0, see LICENSE for details.
14

15
    Licensed under the Apache License, Version 2.0 (the "License");
16
    you may not use this file except in compliance with the License.
17
    You may obtain a copy of the License at
18

19
       http://www.apache.org/licenses/LICENSE-2.0
20

21
    Unless required by applicable law or agreed to in writing, software
22
    distributed under the License is distributed on an "AS IS" BASIS,
23
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
    See the License for the specific language governing permissions and
25
    limitations under the License.
26
"""
27
import collections.abc
1✔
28
import copy
1✔
29
import logging
1✔
30
import json
1✔
31
from pathlib import Path
1✔
32

33
from mslib.utils.qt import get_open_filename, get_save_filename, show_popup
1✔
34
from mslib.msui.qt5 import ui_configuration_editor_window as ui_conf
1✔
35
from PyQt5 import QtWidgets, QtCore, QtGui
1✔
36
from mslib.msui.constants import MSUI_SETTINGS
1✔
37
from mslib.msui.icons import icons
1✔
38
from mslib.utils.config import MSUIDefaultConfig as mss_default
1✔
39
from mslib.utils.config import config_loader, dict_raise_on_duplicates_empty, merge_dict
1✔
40
from mslib.utils.get_projection_params import get_projection_params
1✔
41

42
from mslib.support.qt_json_view import delegate
1✔
43
from mslib.support.qt_json_view.view import JsonView
1✔
44
from mslib.support.qt_json_view.model import JsonModel
1✔
45
from mslib.support.qt_json_view.datatypes import match_type, DataType, TypeRole, ListType
1✔
46

47

48
InvalidityRole = TypeRole + 1
1✔
49
DummyRole = TypeRole + 2
1✔
50
default_options = config_loader(default=True)
1✔
51

52

53
def get_root_index(index, parents=False):
1✔
54
    parent_list = []
1✔
55
    while index.parent().isValid():
1✔
56
        index = index.parent()
×
57
        parent_list.append(index)
×
58
    parent_list.reverse()
1✔
59
    if parents:
1✔
60
        return index, parent_list
1✔
61
    return index
×
62

63

64
class JsonDelegate(delegate.JsonDelegate):
1✔
65

66
    def paint(self, painter, option, index):
1✔
67
        """Use method from the data type or fall back to the default."""
68
        if index.column() == 0:
1✔
69
            source_model = index.model()
1✔
70
            if isinstance(source_model, QtCore.QAbstractProxyModel):
1✔
71
                source_model = source_model.sourceModel()
1✔
72
            data = source_model.serialize()
1✔
73

74
            # bold the key which has non-default value
75
            root_index, parents = get_root_index(index, parents=True)
1✔
76
            parents.append(index)
1✔
77
            key = root_index.data()
1✔
78
            if key in mss_default.list_option_structure or \
1✔
79
                key in mss_default.dict_option_structure or \
80
                key in mss_default.key_value_options:
81
                if root_index == index and data[key] != default_options[key]:
1✔
82
                    option.font.setWeight(QtGui.QFont.Bold)
1✔
83
            elif key in mss_default.fixed_dict_options:
1✔
84
                model_data = data[key]
1✔
85
                default_data = default_options[key]
1✔
86
                for parent in parents[1:]:
1✔
87
                    parent_data = parent.data()
×
88
                    if isinstance(default_data, list):
×
89
                        parent_data = int(parent.data())
×
90
                    model_data = model_data[parent_data]
×
91
                    default_data = default_data[parent_data]
×
92
                if model_data != default_data:
1✔
93
                    option.font.setWeight(QtGui.QFont.Bold)
1✔
94

95
            return super().paint(painter, option, index)
1✔
96

97
        type_ = index.data(TypeRole)
1✔
98
        if isinstance(type_, DataType):
1✔
99
            try:
1✔
100
                super().paint(painter, option, index)
1✔
101
                return type_.paint(painter, option, index)
1✔
102
            except NotImplementedError:
1✔
103
                pass
1✔
104
        return super().paint(painter, option, index)
1✔
105

106

107
class JsonSortFilterProxyModel(QtCore.QSortFilterProxyModel):
1✔
108

109
    def filterAcceptsRow(self, source_row, source_parent):
1✔
110
        # check if an item is currently accepted
111
        accepted = super().filterAcceptsRow(source_row, source_parent)
1✔
112
        if accepted:
1✔
113
            return True
1✔
114

115
        # checking if parent is accepted (works only for indexes with depth 2)
116
        src_model = self.sourceModel()
×
117
        index = src_model.index(source_row, self.filterKeyColumn(), source_parent)
×
118
        has_parent = src_model.itemFromIndex(index).parent()
×
119
        if has_parent:
×
120
            parent_index = self.mapFromSource(has_parent.index())
×
121
            return super().filterAcceptsRow(has_parent.row(), parent_index)
×
122

123
        return accepted
×
124

125

126
class ConfigurationEditorWindow(QtWidgets.QMainWindow, ui_conf.Ui_ConfigurationEditorWindow):
1✔
127
    """MSUI configuration editor class. Provides user interface elements for editing msui_settings.json
128
    """
129

130
    restartApplication = QtCore.pyqtSignal(name="restartApplication")
1✔
131

132
    def __init__(self, parent=None):
1✔
133
        super().__init__(parent)
1✔
134
        self.setupUi(self)
1✔
135

136
        options = config_loader()
1✔
137
        self.path = MSUI_SETTINGS
1✔
138
        self.last_saved = copy.deepcopy(options)
1✔
139

140
        self.optCb.addItem("All")
1✔
141
        for option in sorted(options.keys(), key=str.lower):
1✔
142
            self.optCb.addItem(option)
1✔
143

144
        # Create view and place in parent widget
145
        self.view = JsonView()
1✔
146
        self.view.setItemDelegate(JsonDelegate())
1✔
147
        self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
1✔
148
        self.jsonWidget.setLayout(QtWidgets.QVBoxLayout())
1✔
149
        self.jsonWidget.layout().setContentsMargins(0, 0, 0, 0)
1✔
150
        self.jsonWidget.layout().addWidget(self.view)
1✔
151

152
        # Create proxy model for filtering
153
        self.proxy_model = JsonSortFilterProxyModel()
1✔
154
        self.json_model = JsonModel(data=options, editable_keys=True, editable_values=True)
1✔
155
        self.json_model.setHorizontalHeaderLabels(['Option', 'Value'])
1✔
156

157
        # Set view model
158
        self.proxy_model.setSourceModel(self.json_model)
1✔
159
        self.view.setModel(self.proxy_model)
1✔
160

161
        # Setting proxy model and view attributes
162
        self.proxy_model.setFilterKeyColumn(0)
1✔
163

164
        # Add actions to toolbar
165
        self.import_file_action = QtWidgets.QAction(
1✔
166
            QtGui.QIcon(icons("config_editor", "Folder-new.svg")), "Import config", self)
167
        self.import_file_action.setStatusTip("Import an external configuration file")
1✔
168
        self.toolBar.addAction(self.import_file_action)
1✔
169

170
        self.save_file_action = QtWidgets.QAction(
1✔
171
            QtGui.QIcon(icons("config_editor", "Document-save.svg")), "Save config", self)
172
        self.save_file_action.setStatusTip("Save current configuration")
1✔
173
        self.toolBar.addAction(self.save_file_action)
1✔
174

175
        self.export_file_action = QtWidgets.QAction(
1✔
176
            QtGui.QIcon(icons("config_editor", "Document-save-as.svg")), "Export config", self)
177
        self.export_file_action.setStatusTip("Export current configuration")
1✔
178
        self.toolBar.addAction(self.export_file_action)
1✔
179

180
        # Button signals
181
        self.optCb.currentIndexChanged.connect(self.set_option_filter)
1✔
182
        self.addOptBtn.clicked.connect(self.add_option_handler)
1✔
183
        self.removeOptBtn.clicked.connect(self.remove_option_handler)
1✔
184
        self.restoreDefaultsBtn.clicked.connect(self.restore_defaults)
1✔
185
        self.moveUpTb.clicked.connect(lambda: self.move_option(move=1))
1✔
186
        self.moveDownTb.clicked.connect(lambda: self.move_option(move=-1))
1✔
187

188
        # File action signals
189
        self.import_file_action.triggered.connect(self.import_config)
1✔
190
        self.save_file_action.triggered.connect(self.save_config)
1✔
191
        self.export_file_action.triggered.connect(self.export_config)
1✔
192

193
        # View/Model signals
194
        self.view.selectionModel().selectionChanged.connect(self.tree_selection_changed)
1✔
195
        self.json_model.dataChanged.connect(self.update_view)
1✔
196

197
        # set behaviour of widgets
198
        self.moveUpTb.hide()
1✔
199
        self.moveDownTb.hide()
1✔
200
        self.moveUpTb.setAutoRaise(True)
1✔
201
        self.moveUpTb.setArrowType(QtCore.Qt.UpArrow)
1✔
202
        self.moveUpTb.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
1✔
203
        self.moveDownTb.setAutoRaise(True)
1✔
204
        self.moveDownTb.setArrowType(QtCore.Qt.DownArrow)
1✔
205
        self.moveDownTb.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
1✔
206

207
        self.moveUpTb.setEnabled(False)
1✔
208
        self.moveDownTb.setEnabled(False)
1✔
209
        self.addOptBtn.setEnabled(False)
1✔
210
        self.removeOptBtn.setEnabled(False)
1✔
211
        self.restoreDefaultsBtn.setEnabled(False)
1✔
212

213
        # set tooltip and make keys non-editable
214
        self.set_noneditable_items(QtCore.QModelIndex())
1✔
215

216
        # json view attributes
217
        self.view.setAlternatingRowColors(True)
1✔
218
        self.view.setColumnWidth(0, self.view.width() // 2)
1✔
219

220
        # Add invalidity roles and update status of keys
221
        self.update_view()
1✔
222

223
        self.restart_on_save = True
1✔
224

225
    def set_noneditable_items(self, parent):
1✔
226
        for r in range(self.json_model.rowCount(parent)):
1✔
227
            index = self.json_model.index(r, 0, parent)
1✔
228
            item = self.json_model.itemFromIndex(index)
1✔
229
            item.setEditable(False)
1✔
230
            if item.text() in mss_default.fixed_dict_options + mss_default.fixed_list_options:
1✔
231
                self.set_noneditable_items(index)
1✔
232
            if item.text() in mss_default.config_descriptions:
1✔
233
                item.setData(mss_default.config_descriptions[item.text()], QtCore.Qt.ToolTipRole)
1✔
234

235
    def tree_selection_changed(self, selected, deselected):
1✔
236
        """Enable/Disable appropriate buttons based on selection in treeview
237
        """
238
        selection = self.view.selectionModel().selectedRows()
×
239
        # if no selection
240
        add, remove, restore_defaults, move = [False] * 4
×
241
        if len(selection) == 1:
×
242
            index = selection[0]
×
243
            if not index.parent().isValid():
×
244
                move = True
×
245
            root_index = get_root_index(index)
×
246
            if (root_index.data() not in mss_default.fixed_dict_options + mss_default.key_value_options +
×
247
                    mss_default.fixed_list_options):
248
                add, move = True, True
×
249

250
            # display error message if key has invalid values
251
            if not index.parent().isValid():
×
252
                root_index = get_root_index(index)
×
253
                source_index = self.proxy_model.mapToSource(root_index)
×
254
                item = self.json_model.itemFromIndex(source_index)
×
255
                if any(item.data(InvalidityRole)):
×
256
                    invalidity = item.data(InvalidityRole)
×
257
                    errors = {"empty": invalidity[0], "duplicate": invalidity[1], "invalid": invalidity[2]}
×
258
                    msg = ", ".join([key for key in errors if errors[key]])
×
259
                    msg += " values found"
×
260
                    self.statusbar.showMessage(msg)
×
261
                elif item.data(DummyRole):
×
262
                    self.statusbar.showMessage("Dummy values found")
×
263
                else:
264
                    self.statusbar.showMessage("")
×
265
        if len(selection) >= 1:
×
266
            restore_defaults = True
×
267
            for index in selection:
×
268
                index = get_root_index(index)
×
269
                if index.data() not in mss_default.fixed_dict_options + mss_default.key_value_options  \
×
270
                        + mss_default.fixed_list_options and self.proxy_model.rowCount(index) > 0:
271
                    remove = True
×
272
                    break
×
273

274
        self.addOptBtn.setEnabled(add)
×
275
        self.removeOptBtn.setEnabled(remove)
×
276
        self.restoreDefaultsBtn.setEnabled(restore_defaults)
×
277
        self.moveUpTb.setEnabled(move)
×
278
        self.moveDownTb.setEnabled(move)
×
279

280
    def update_view(self):
1✔
281
        """
282
        Set InvalidRole and DummyRole for all root items in the treeview and highlight appropriately.
283

284
        InvalidRole -> Boolean list indicating if item has Empty, Duplicate, Invalid values
285
        DummyRole -> Boolean value indicating if item has dummy value
286
        """
287
        source_model = self.json_model
1✔
288
        data = source_model.serialize()
1✔
289
        parent = QtCore.QModelIndex()
1✔
290
        for r in range(source_model.rowCount(parent)):
1✔
291
            root_index = source_model.index(r, 0, parent)
1✔
292
            root_item = source_model.itemFromIndex(root_index)
1✔
293

294
            empty, duplicate, invalid, dummy = [False] * 4
1✔
295
            color = QtCore.Qt.transparent
1✔
296
            key = root_index.data()
1✔
297
            if key in mss_default.dict_option_structure:
1✔
298
                child_keys = set()
1✔
299
                rows = source_model.rowCount(root_index)
1✔
300
                for row in range(rows):
1✔
301
                    child_key_data = source_model.index(row, 0, root_index).data()
1✔
302
                    child_keys.add(child_key_data)
1✔
303
                    if child_key_data == "":
1✔
304
                        empty = True
×
305

306
                # check for dummy values
307
                default = mss_default.dict_option_structure[key]
1✔
308
                values_dict = data[key]
1✔
309
                for value in values_dict:
1✔
310
                    if value in default:
1✔
311
                        if default[value] == values_dict[value]:
×
312
                            dummy = True
×
313
                            color = QtCore.Qt.gray
×
314
                            break
×
315

316
                # condition for checking duplicate and empty keys
317
                if len(child_keys) != rows or empty:
1✔
318
                    duplicate = True
×
319
                    color = QtCore.Qt.red
×
320
            elif key in mss_default.list_option_structure:
1✔
321
                values_list = data[key]
1✔
322
                # check if any dummy values
323
                if any([value == mss_default.list_option_structure[key][0] for value in values_list]):
1✔
324
                    dummy = True
×
325
                    color = QtCore.Qt.gray
×
326
                # check if any empty values
327
                if any([value == "" for value in values_list]):
1✔
328
                    empty = True
×
329
                    color = QtCore.Qt.red
×
330
                # check if any duplicate values
331
                if len(set([tuple(_x) if isinstance(_x, list) else _x for _x in values_list])) != len(values_list):
1✔
332
                    duplicate = True
×
333
                    color = QtCore.Qt.red
×
334
            elif key == 'filepicker_default':
1✔
335
                if data[key] not in ['default', 'qt']:
1✔
UNCOV
336
                    invalid = True
×
337
                    color = QtCore.Qt.red
×
338

339
            # set invalidityroles and dummyrole for key
340
            root_item.setData([empty, duplicate, invalid], InvalidityRole)
1✔
341
            root_item.setData(dummy, DummyRole)
1✔
342
            # set color for column 1
343
            item = source_model.itemFromIndex(root_index)
1✔
344
            item.setBackground(color)
1✔
345
            # set color for column 2
346
            source_index = source_model.index(r, 1, parent)
1✔
347
            item = source_model.itemFromIndex(source_index)
1✔
348
            item.setBackground(color)
1✔
349

350
    def set_option_filter(self, index):
1✔
351
        # By default FilterKeyColumn of the proxy model is set to 0
UNCOV
352
        if self.optCb.currentText() == "All":
×
353
            self.proxy_model.setFilterRegExp("")
×
354
            return
×
355
        self.proxy_model.setFilterRegExp(QtCore.QRegExp(f"^{self.optCb.currentText()}$"))
×
356
        self.view.expandAll()
×
357

358
    def add_option_handler(self):
1✔
UNCOV
359
        selection = self.view.selectionModel().selectedRows()
×
360
        if len(selection) == 0 or len(selection) > 1:
×
361
            logging.debug("zero or multiple selections while trying to add new value")
×
362
            self.statusbar.showMessage("Please select one option to add new value")
×
363
            return
×
364

UNCOV
365
        selected_index = get_root_index(selection[0])
×
366
        option = selected_index.data()
×
367
        parent = QtCore.QModelIndex()
×
368
        for r in range(self.json_model.rowCount(parent)):
×
369
            index = self.json_model.index(r, 0, parent)
×
370
            item = self.json_model.itemFromIndex(index)
×
371
            if index.data() == option:
×
372
                if option in mss_default.fixed_dict_options + mss_default.key_value_options:
×
373
                    # Cannot add options to fixed structure options
UNCOV
374
                    self.statusbar.showMessage(
×
375
                        "Option already exists. Please change value to your preference or restore to default.")
UNCOV
376
                    return
×
377
                elif option in mss_default.dict_option_structure:
×
378
                    # Append dummy value dict to options having a dictionary structure
UNCOV
379
                    json_data = mss_default.dict_option_structure[option]
×
380
                    type_ = match_type(json_data)
×
381
                    type_.next(model=self.json_model, data=json_data, parent=item)
×
382
                elif option in mss_default.list_option_structure:
×
383
                    # Append dummy value to options having a list structure
UNCOV
384
                    json_data = mss_default.list_option_structure[option]
×
385
                    type_ = match_type(json_data)
×
386
                    type_.next(model=self.json_model, data=json_data, parent=item)
×
387
                    # increase row count in view
UNCOV
388
                    rows = self.json_model.rowCount(index) - 1
×
389
                    new_item = self.json_model.itemFromIndex(self.json_model.index(rows, 0, index))
×
390
                    new_item.setData(rows, QtCore.Qt.DisplayRole)
×
391
                self.statusbar.showMessage("")
×
392
                # expand root item
UNCOV
393
                proxy_index = self.proxy_model.mapFromSource(index)
×
394
                self.view.expand(proxy_index)
×
395
                # expand, scroll to and select new item
UNCOV
396
                rows = self.json_model.rowCount(index) - 1
×
397
                new_index = self.json_model.index(rows, 0, index)
×
398
                proxy_index = self.proxy_model.mapFromSource(new_index)
×
399
                self.view.expand(proxy_index)
×
400
                self.view.scrollTo(proxy_index)
×
401
                self.view.selectionModel().select(
×
402
                    proxy_index, QtCore.QItemSelectionModel.ClearAndSelect | QtCore.QItemSelectionModel.Rows)
UNCOV
403
                logging.debug("Added new value for %s", option)
×
404
                self.update_view()
×
405
                break
×
406

407
    def remove_option_handler(self):
1✔
UNCOV
408
        selection = self.view.selectionModel().selectedRows()
×
409
        if len(selection) == 0:
×
410
            logging.debug("zero selections while trying to remove option")
×
411
            self.statusbar.showMessage("Please select one/more options to remove")
×
412
            return
×
413

414
        # Collect all removable indexes from selected items
UNCOV
415
        non_removable = []
×
416
        removable_indexes = {}
×
417
        for index in selection:
×
418
            if not index.parent().isValid():
×
419
                if index.data() not in mss_default.fixed_dict_options + mss_default.key_value_options:
×
420
                    removable_indexes[index] = set(range(self.proxy_model.rowCount(index)))
×
421
                else:
422
                    # cannot remove root item
UNCOV
423
                    non_removable.append(index)
×
424
            else:
425
                # find penultimate option key
UNCOV
426
                while index.parent().parent().isValid():
×
427
                    index = index.parent()
×
428
                root = index.parent()
×
429
                # enter only if root option not in fixed dictionary / key value options
UNCOV
430
                if root.data() not in mss_default.fixed_dict_options + mss_default.key_value_options:
×
431
                    if root in removable_indexes:
×
432
                        removable_indexes[root].add(index.row())
×
433
                    else:
UNCOV
434
                        removable_indexes[root] = {index.row()}
×
435
                else:
UNCOV
436
                    non_removable.append(index)
×
437

UNCOV
438
        if removable_indexes == {} and non_removable != []:
×
439
            self.statusbar.showMessage("Default options are not removable.")
×
440
            return
×
441

442
        # ToDo add confirmation dialog here
443

UNCOV
444
        options = "\n".join([index.data() for index in removable_indexes])
×
445
        logging.debug("Attempting to remove the following options\n%s", options)
×
446

UNCOV
447
        self.view.selectionModel().clearSelection()
×
448
        for index in removable_indexes:
×
449
            rows = sorted(list(removable_indexes[index]))
×
450
            for count, row in enumerate(rows):
×
451
                row = row - count
×
452
                self.proxy_model.removeRow(row, parent=index)
×
453

454
            # fix row number in list type options
UNCOV
455
            source_index = self.proxy_model.mapToSource(index)
×
456
            source_item = self.json_model.itemFromIndex(source_index)
×
457
            if isinstance(source_item.data(QtCore.Qt.UserRole + 1), ListType):
×
458
                for r in range(self.json_model.rowCount(source_index)):
×
459
                    child_index = self.json_model.index(r, 0, source_index)
×
460
                    item = self.json_model.itemFromIndex(child_index)
×
461
                    item.setData(r, QtCore.Qt.DisplayRole)
×
462

UNCOV
463
        self.statusbar.showMessage("Successfully removed values selected options")
×
464
        self.update_view()
×
465

466
    def restore_defaults(self):
1✔
UNCOV
467
        def update(data, option, value):
×
468
            """Function to update dict at a depth"""
UNCOV
469
            for k, v in data.items():
×
470
                if k == option:
×
471
                    data[k] = value
×
472
                    break
×
473
                if isinstance(v, collections.abc.Mapping):
×
474
                    data[k] = update(data.get(k, {}), option, value)
×
475
            return data
×
476

UNCOV
477
        selection = self.view.selectionModel().selectedRows()
×
478
        if len(selection) == 0:
×
479
            logging.debug("no selections while trying to restore defaults")
×
480
            self.statusbar.showMessage("Please select one/more options to restore defaults")
×
481
            return
×
482

483
        # get list of distinct indexes to restore
UNCOV
484
        model_data = self.json_model.serialize()
×
485
        selected_indexes = set()
×
486
        for index in selection:
×
487
            root_index, parent_list = get_root_index(index, parents=True)
×
488
            added = False
×
489
            data = model_data
×
490
            for parent in parent_list + [index]:
×
491
                data = data[parent.data()]
×
492
                if isinstance(data, list):
×
493
                    added = True
×
494
                    selected_indexes.add(parent)
×
495
                    break
×
496
            if not added:
×
497
                selected_indexes.add(index)
×
498

499
        # ToDo add confirmation dialog here
500

UNCOV
501
        options = "\n".join([index.data() for index in selected_indexes])
×
502
        logging.debug("Attempting to restore defaults for the following options\n%s", options)
×
503

UNCOV
504
        for index in selected_indexes:
×
505
            # check if root option and present in mss_default.key_value_options
UNCOV
506
            if not index.parent().isValid() and index.data() in mss_default.key_value_options:
×
507
                value_index = self.json_model.index(index.row(), 1, QtCore.QModelIndex())
×
508
                value_item = self.json_model.itemFromIndex(value_index)
×
509
                value_item.setData(default_options[index.data()], QtCore.Qt.DisplayRole)
×
510
                continue
511

UNCOV
512
            root_index, parent_list = get_root_index(index, parents=True)
×
513
            option = root_index.data()
×
514
            model_data = self.json_model.serialize()
×
515
            if option in mss_default.fixed_dict_options:
×
516
                if index == root_index:
×
517
                    json_data = default_options[option]
×
518
                else:
UNCOV
519
                    key = None
×
520
                    value = copy.deepcopy(default_options)
×
521
                    for parent in parent_list + [index]:
×
522
                        parent_data = parent.data()
×
523
                        if isinstance(value, list):
×
524
                            break
×
525
                        key = parent_data
×
526
                        value = value[parent_data]
×
527
                    data = copy.deepcopy(model_data[option])
×
528
                    json_data = update(data, key, value)
×
529
            else:
UNCOV
530
                json_data = default_options[option]
×
531
            if model_data[option] == json_data:
×
532
                continue
533
            # remove all rows
UNCOV
534
            for row in range(self.proxy_model.rowCount(root_index)):
×
535
                self.proxy_model.removeRow(0, parent=root_index)
×
536
            # add default values
UNCOV
537
            source_index = self.proxy_model.mapToSource(root_index)
×
538
            source_item = self.json_model.itemFromIndex(source_index)
×
539
            type_ = match_type(json_data)
×
540
            type_.next(model=self.json_model, data=json_data, parent=source_item)
×
541

UNCOV
542
        self.statusbar.showMessage("Defaults restored for selected options")
×
543
        self.view.clearSelection()
×
544
        self.update_view()
×
545

546
    def import_config(self):
1✔
UNCOV
547
        file_path = get_open_filename(self, "Import config", "", ";;".join(["JSON Files (*.json)", "All Files (*.*)"]))
×
548
        if not file_path:
×
549
            return
×
550

UNCOV
551
        if Path(file_path).exists():
×
552
            self.statusbar.showMessage("Importing config from path")
×
553
            file_content = Path(file_path).read_text(encoding="utf8")
×
554
            try:
×
555
                json_file_data = json.loads(file_content, object_pairs_hook=dict_raise_on_duplicates_empty)
×
556
            except json.JSONDecodeError as e:
×
557
                show_popup(self, "Error while loading file", e)
×
558
                logging.error("Error while loading json file %s", e)
×
559
                return
×
560
            except ValueError as e:
×
561
                show_popup(self, "Invalid keys detected", e)
×
562
                logging.error("Error while loading json file %s", e)
×
563
                return
×
564

UNCOV
565
        if json_file_data:
×
566
            json_model_data = self.json_model.serialize()
×
567
            options = merge_dict(copy.deepcopy(json_model_data), json_file_data)
×
568
            if options == json_model_data:
×
569
                self.statusbar.showMessage("No option with new values found")
×
570
                return
×
571
            # replace existing data with new data
UNCOV
572
            self.json_model.init(options, editable_keys=True, editable_values=True)
×
573
            self.view.setColumnWidth(0, self.view.width() // 2)
×
574
            self.set_noneditable_items(QtCore.QModelIndex())
×
575
            self.update_view()
×
576
            self.statusbar.showMessage("Successfully imported config")
×
577
            logging.debug("Imported new config data from file")
×
578
        else:
UNCOV
579
            self.statusbar.showMessage("No data found in the file")
×
580
            logging.debug("No data found in the file, using existing settings")
×
581

582
    def _save_to_path(self, filename):
1✔
UNCOV
583
        self.last_saved = self.json_model.serialize()
×
584
        json_data = copy.deepcopy(self.last_saved)
×
585
        save_data = copy.deepcopy(self.last_saved)
×
586

UNCOV
587
        for key in json_data:
×
588
            if json_data[key] == default_options[key] or json_data[key] == {} or json_data[key] == []:
×
589
                del save_data[key]
×
590

591
        # ToDo check errors keyword
UNCOV
592
        Path(filename).write_text(json.dumps(save_data, indent=4), encoding="utf8", errors="ignore")
×
593

594
    def validate_data(self):
1✔
595
        epsg_check, dummy = self.problem_in_map_sections()
1✔
596
        if epsg_check:
1✔
UNCOV
597
            return epsg_check, dummy
×
598
        invalid, dummy = False, False
1✔
599

600
        parent = QtCore.QModelIndex()
1✔
601
        for r in range(self.json_model.rowCount(parent)):
1✔
602
            index = self.json_model.index(r, 0, parent)
1✔
603
            item = self.json_model.itemFromIndex(index)
1✔
604
            invalid |= any(item.data(InvalidityRole))
1✔
605
            dummy |= item.data(DummyRole)
1✔
606

607
        return invalid, dummy
1✔
608

609
    def problem_in_map_sections(self):
1✔
610
        """
611
        Checks for failures in the predefined_map_sections, e.g. not supported epsg codes
612
        """
613
        key = "predefined_map_sections"
1✔
614
        source_model = self.json_model
1✔
615
        # set default color
616
        color = QtCore.Qt.black
1✔
617
        self.set_key_color(source_model, key, color)
1✔
618

619
        data = source_model.serialize()
1✔
620
        predefined_map_sections = data.get(key, dict())
1✔
621
        for name in predefined_map_sections:
1✔
622
            try:
1✔
623
                get_projection_params(predefined_map_sections[name]["CRS"])
1✔
UNCOV
624
            except ValueError:
×
625
                # visualize failure in section
UNCOV
626
                color = QtCore.Qt.red
×
627
                self.set_key_color(source_model, key, color)
×
628
                return True, True
×
629

630
        return False, False
1✔
631

632
    def set_key_color(self, source_model, key, color):
1✔
633
        """
634
        sets the foreground color of an item
635
        """
636
        parent = QtCore.QModelIndex()
1✔
637
        for r in range(source_model.rowCount(parent)):
1✔
638
            root_index = source_model.index(r, 0, parent)
1✔
639
            if key == root_index.data():
1✔
640
                item = source_model.itemFromIndex(root_index)
1✔
641
                item.setForeground(color)
1✔
642
                break
1✔
643

644
    def check_modified(self):
1✔
645
        return not self.last_saved == self.json_model.serialize()
1✔
646

647
    def save_config(self):
1✔
UNCOV
648
        invalid, dummy = self.validate_data()
×
649
        if invalid:
×
650
            show_popup(
×
651
                self,
652
                "Invalid values detected",
653
                "Please correct the invalid values (keys colored in red) to be able to save.")
UNCOV
654
            self.statusbar.showMessage("Please correct the values and try saving again")
×
655
            return False
×
656
        if dummy and self.check_modified():
×
657
            ret = QtWidgets.QMessageBox.warning(
658
                self, self.tr("Dummy values detected"),
659
                self.tr("Dummy values detected (keys colored in gray.)\n"
660
                        "Since they are dummy values you might face issues later on while working."
661
                        "\n\nDo you still want to continue to save?"),
662
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
663
                QtWidgets.QMessageBox.No)
UNCOV
664
            if ret == QtWidgets.QMessageBox.No:
×
665
                self.statusbar.showMessage("Please correct the values and try saving")
×
666
                return False
×
667

UNCOV
668
        if self.check_modified():
×
669
            if self.restart_on_save:
×
670
                ret = QtWidgets.QMessageBox.warning(
×
671
                    self, self.tr("Mission Support System"),
672
                    self.tr("Do you want to restart the application?\n"
673
                            "(This is necessary to apply changes)\n\n"
674
                            "Please note that clicking 'No' will not save the current configuration"),
675
                    QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
676
                    QtWidgets.QMessageBox.No)
UNCOV
677
                if ret == QtWidgets.QMessageBox.Yes:
×
678
                    logging.debug("saving config file to: %s and restarting MSS", self.path)
×
679
                    self._save_to_path(self.path)
×
680
                    self.restartApplication.emit()
×
681
                    self.restart_on_save = False
×
682
                    self.close()
×
683
                else:
UNCOV
684
                    return
×
685
            self.restart_on_save = True
×
686
            logging.debug("saving config file to: %s", self.path)
×
687
            self._save_to_path(self.path)
×
688
        else:
UNCOV
689
            self.statusbar.showMessage("No values changed")
×
690
        return True
×
691

692
    def export_config(self):
1✔
UNCOV
693
        invalid, dummy = self.validate_data()
×
694
        if invalid:
×
695
            show_popup(
×
696
                self,
697
                "Invalid values detected",
698
                "Please correct the invalid values (keys colored in red) to be able to save.")
UNCOV
699
            self.statusbar.showMessage("Please correct the values and try exporting")
×
700
            return False
×
701

UNCOV
702
        if self.json_model.serialize() == default_options:
×
703
            msg = """Since the current configuration matches the default configuration, \
704
only an empty json file would be exported.\nDo you still want to continue?"""
UNCOV
705
            ret = QtWidgets.QMessageBox.warning(
×
706
                self, self.tr("Mission Support System"), self.tr(msg),
707
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
708
                QtWidgets.QMessageBox.No)
UNCOV
709
            if ret == QtWidgets.QMessageBox.No:
×
710
                return
×
711

UNCOV
712
        path = get_save_filename(self, "Export configuration", "msui_settings", "JSON files (*.json)")
×
713
        if path:
×
714
            self._save_to_path(path)
×
715

716
    def closeEvent(self, event):
1✔
717
        msg = ""
1✔
718
        invalid, dummy = self.validate_data()
1✔
719
        if invalid:
1✔
UNCOV
720
            msg = self.tr("Invalid keys/values found in config.\nDo you want to rectify and save changes?")
×
721
        elif dummy and not self.check_modified:
1✔
UNCOV
722
            msg = self.tr("Dummy keys/values found in config.\nDo you want to rectify and save changes?")
×
723
        elif self.check_modified():
1✔
UNCOV
724
            msg = self.tr(
×
725
                "Save Changes to default msui_settings.json?\nYou need to restart the gui for changes to take effect.")
726
        if msg != "":
1✔
UNCOV
727
            ret = QtWidgets.QMessageBox.warning(
×
728
                self, self.tr("Mission Support System"), self.tr(msg),
729
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
730
                QtWidgets.QMessageBox.No)
UNCOV
731
            if ret == QtWidgets.QMessageBox.Yes:
×
732
                if not self.save_config():
×
733
                    event.ignore()
×
734
                    return
×
735
        elif self.restart_on_save:
1✔
736
            ret = QtWidgets.QMessageBox.warning(
1✔
737
                self, self.tr("Mission Support System"),
738
                self.tr("Do you want to close the config editor?"),
739
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
740
                QtWidgets.QMessageBox.No)
741
            if ret == QtWidgets.QMessageBox.No:
1✔
UNCOV
742
                event.ignore()
×
743
                return
×
744

745
        event.accept()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc