Skip to content

Commit 7db97be

Browse files
warning before saving file
1 parent 125549a commit 7db97be

File tree

7 files changed

+193
-80
lines changed

7 files changed

+193
-80
lines changed

pb_tool.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ extras: metadata.txt icon.png README.md LICENSE
6464

6565
# Other directories to be deployed with the plugin.
6666
# These must be subdirectories under the plugin directory
67-
extra_dirs: models/ ui_widgets/
67+
extra_dirs: models/ ui_widgets/ utils/
6868

6969
# ISO code(s) for any locales (translations), separated by spaces.
7070
# Corresponding .ts files must exist in the i18n directory

pygeoapi_config_dialog.py

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@
2222
***************************************************************************/
2323
"""
2424

25+
from copy import deepcopy
2526
from datetime import datetime, timezone
2627
import os
2728
import yaml
2829

30+
from .utils.data_diff import diff_yaml_dict
31+
2932
from .ui_widgets.utils import get_url_status
3033

3134

@@ -46,6 +49,7 @@
4649
QFileDialog,
4750
QMessageBox,
4851
QDialogButtonBox,
52+
QDialog,
4953
QApplication,
5054
) # or PyQt6.QtWidgets
5155

@@ -99,6 +103,7 @@ def __init__(self, parent=None):
99103
# #widgets-and-dialogs-with-auto-connect
100104
self.setupUi(self)
101105
self.config_data = ConfigData()
106+
self.yaml_original_data = None
102107
self.ui_setter = UiSetter(self)
103108
self.data_from_ui_setter = DataSetterFromUi(self)
104109

@@ -135,6 +140,31 @@ def represent_datetime_as_timestamp(dumper, data: datetime):
135140
self.ui_setter.set_ui_from_data()
136141
self.ui_setter.setup_map_widget()
137142

143+
def on_button_clicked(self, button):
144+
145+
role = self.buttonBox.buttonRole(button)
146+
print(f"Button clicked: {button.text()}, Role: {role}")
147+
148+
# You can also check the standard button type
149+
if button == self.buttonBox.button(QDialogButtonBox.Save):
150+
if self._set_validate_ui_data()[0]:
151+
file_path, _ = QFileDialog.getSaveFileName(
152+
self, "Save File", "", "YAML Files (*.yml);;All Files (*)"
153+
)
154+
155+
# before saving, show diff with "Procced" and "Cancel" options
156+
if self._diff_original_and_current_data():
157+
self.save_to_file(file_path)
158+
159+
elif button == self.buttonBox.button(QDialogButtonBox.Open):
160+
file_name, _ = QFileDialog.getOpenFileName(
161+
self, "Open File", "", "YAML Files (*.yml);;All Files (*)"
162+
)
163+
self.open_file(file_name)
164+
165+
elif button == self.buttonBox.button(QDialogButtonBox.Close):
166+
self.reject()
167+
138168
def save_to_file(self, file_path):
139169

140170
if file_path:
@@ -179,8 +209,10 @@ def open_file(self, file_name):
179209
self.config_data = ConfigData()
180210

181211
# set data and .all_missing_props:
182-
self.yaml_original_data = yaml.safe_load(file_content)
183-
self.config_data.set_data_from_yaml(self.yaml_original_data)
212+
yaml_original_data = yaml.safe_load(file_content)
213+
self.yaml_original_data = deepcopy(yaml_original_data)
214+
215+
self.config_data.set_data_from_yaml(yaml_original_data)
184216

185217
# set UI from data
186218
self.ui_setter.set_ui_from_data()
@@ -215,26 +247,6 @@ def open_file(self, file_name):
215247
# finally:
216248
# QApplication.restoreOverrideCursor()
217249

218-
def on_button_clicked(self, button):
219-
220-
role = self.buttonBox.buttonRole(button)
221-
print(f"Button clicked: {button.text()}, Role: {role}")
222-
223-
# You can also check the standard button type
224-
if button == self.buttonBox.button(QDialogButtonBox.Save):
225-
if self._set_validate_ui_data()[0]:
226-
file_path, _ = QFileDialog.getSaveFileName(
227-
self, "Save File", "", "YAML Files (*.yml);;All Files (*)"
228-
)
229-
self.save_to_file(file_path)
230-
elif button == self.buttonBox.button(QDialogButtonBox.Open):
231-
file_name, _ = QFileDialog.getOpenFileName(
232-
self, "Open File", "", "YAML Files (*.yml);;All Files (*)"
233-
)
234-
self.open_file(file_name)
235-
elif button == self.buttonBox.button(QDialogButtonBox.Close):
236-
self.reject()
237-
238250
def _set_validate_ui_data(self) -> tuple[bool, list]:
239251
# Set and validate data from UI
240252
try:
@@ -263,6 +275,49 @@ def _set_validate_ui_data(self) -> tuple[bool, list]:
263275
QMessageBox.warning(f"Error deserializing: {e}")
264276
return
265277

278+
def _diff_original_and_current_data(self) -> tuple[bool, list]:
279+
"""Before saving the file, show the diff and give an option to proceed or cancel."""
280+
if not self.yaml_original_data:
281+
return True
282+
283+
diff_data = diff_yaml_dict(
284+
self.yaml_original_data,
285+
self.config_data.asdict_enum_safe(self.config_data),
286+
)
287+
288+
# remove from diff removed nulls, and originally missing props
289+
new_removed_dict = {}
290+
for k, v in diff_data["removed"].items():
291+
if v is not None and k not in self.config_data.all_missing_props:
292+
new_removed_dict[k] = v
293+
diff_data["removed"] = new_removed_dict
294+
295+
# remove from diff changed values, originally warned about
296+
new_changed_dict = {}
297+
for k, v in diff_data["changed"].items():
298+
if v is None and k in self.config_data.all_missing_props:
299+
continue
300+
new_changed_dict[k] = v
301+
diff_data["changed"] = new_changed_dict
302+
303+
if (
304+
len(diff_data["added"])
305+
+ len(diff_data["removed"])
306+
+ len(diff_data["changed"])
307+
== 0
308+
):
309+
return True
310+
311+
# add a window with the choice
312+
QgsMessageLog.logMessage(f"{diff_data}")
313+
dialog = ReadOnlyTextDialog(self, "Warning", diff_data, True)
314+
result = dialog.exec_() # returns QDialog.Accepted (1) or QDialog.Rejected (0)
315+
316+
if result == QDialog.Accepted:
317+
return True
318+
else:
319+
return False
320+
266321
def open_templates_path_dialog(self):
267322
"""Defining Server.templates.path path, called from .ui file."""
268323

tests/test_yaml_save.py

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from copy import deepcopy
22
import yaml
3-
from typing import Any
43
import pytest
54
import subprocess
65
from pathlib import Path
6+
7+
from ..utils.data_diff import diff_yaml_dict
78
from ..pygeoapi_config_dialog import PygeoapiConfigDialog
89

910
BASE_DIR = Path(__file__).parent / "yaml_samples"
@@ -114,12 +115,19 @@ def test_open_file_validate_ui_data_save_file(qtbot, sample_yaml: str):
114115
# 1. Exclude removed values that are None - not important
115116
# 2. Exclude values that already triggered warning on opening: .all_missing_props
116117
new_removed_dict = {}
117-
118118
for k, v in diff_data["removed"].items():
119119
if v is not None and k not in yaml1_missing_props:
120120
new_removed_dict[k] = v
121121
diff_data["removed"] = new_removed_dict
122122

123+
# 3. Exclude changed values, originally warned about
124+
new_changed_dict = {}
125+
diff_data["changed"] = new_changed_dict
126+
for k, v in diff_data["changed"].items():
127+
if k not in yaml1_missing_props:
128+
new_changed_dict[k] = v
129+
diff_data["changed"] = new_changed_dict
130+
123131
# save to file
124132
diff_yaml_path = sample_yaml.with_name(f"saved_DIFF_{sample_yaml.name}")
125133
with open(diff_yaml_path, "w", encoding="utf-8") as file:
@@ -143,47 +151,3 @@ def test_open_file_validate_ui_data_save_file(qtbot, sample_yaml: str):
143151
assert (
144152
False
145153
), f"YAML data changed after saving: '{sample_yaml.name}'. \nAdded: {len(diff_data['added'])} fields, changed: {len(diff_data['changed'])} fields, removed: {len(diff_data['removed'])} fields."
146-
147-
148-
def diff_yaml_dict(obj1: Any, obj2: Any) -> dict:
149-
"""Returns all added, removed or changed elements between 2 dictionaries."""
150-
151-
diff = {"added": {}, "removed": {}, "changed": {}}
152-
return diff_obj(obj1, obj2, diff, "")
153-
154-
155-
def diff_obj(obj1: Any, obj2: Any, diff: dict, path: str = "") -> dict:
156-
"""Returns all added, removed or changed elements between 2 objects.
157-
Ignores diff in dict keys order. For lists, order is checked."""
158-
159-
if isinstance(obj1, dict) and isinstance(obj2, dict):
160-
all_keys = set(obj1.keys()) | set(obj2.keys())
161-
for key in all_keys:
162-
new_path = f"{path}.{key}" if path else key
163-
if key not in obj1:
164-
diff["added"][new_path] = obj2[key]
165-
elif key not in obj2:
166-
diff["removed"][new_path] = obj1[key]
167-
else:
168-
nested = diff_obj(obj1[key], obj2[key], diff, new_path)
169-
for k in diff:
170-
diff[k].update(nested[k])
171-
172-
elif isinstance(obj1, list) and isinstance(obj2, list):
173-
max_len = max(len(obj1), len(obj2))
174-
for i in range(max_len):
175-
new_path = f"{path}[{i}]"
176-
if i >= len(obj1):
177-
diff["added"][new_path] = obj2[i]
178-
elif i >= len(obj2):
179-
diff["removed"][new_path] = obj1[i]
180-
else:
181-
nested = diff_obj(obj1[i], obj2[i], diff, new_path)
182-
for k in diff:
183-
diff[k].update(nested[k])
184-
185-
else:
186-
if obj1 != obj2:
187-
diff["changed"][path] = {"old": obj1, "new": obj2}
188-
189-
return diff

ui_widgets/DataSetterFromUi.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,29 @@ def set_data_from_ui(self):
6868
config_data.server.gzip = get_enum_value_from_string(
6969
ServerOptionalBoolsEnum, dialog.comboBoxGzip.currentText()
7070
)
71+
if config_data.server.gzip == ServerOptionalBoolsEnum.NONE:
72+
config_data.server.gzip = None
7173

7274
# pretty print
7375
config_data.server.pretty_print = get_enum_value_from_string(
7476
ServerOptionalBoolsEnum, dialog.comboBoxPretty.currentText()
7577
)
78+
if config_data.server.pretty_print == ServerOptionalBoolsEnum.NONE:
79+
config_data.server.pretty_print = None
7680

7781
# admin
7882
config_data.server.admin = get_enum_value_from_string(
7983
ServerOptionalBoolsEnum, dialog.comboBoxAdmin.currentText()
8084
)
85+
if config_data.server.admin == ServerOptionalBoolsEnum.NONE:
86+
config_data.server.admin = None
8187

8288
# cors
8389
config_data.server.cors = get_enum_value_from_string(
8490
ServerOptionalBoolsEnum, dialog.comboBoxCors.currentText()
8591
)
92+
if config_data.server.cors == ServerOptionalBoolsEnum.NONE:
93+
config_data.server.cors = None
8694

8795
# templates
8896
if is_valid_string(dialog.lineEditTemplatesPath.text()) or is_valid_string(
@@ -107,6 +115,8 @@ def set_data_from_ui(self):
107115
config_data.server.language = get_enum_value_from_string(
108116
Languages, dialog.comboBoxLangSingle.currentText()
109117
)
118+
if config_data.server.language == Languages.NONE:
119+
config_data.server.language = None
110120

111121
# languages
112122
config_data.server.languages = []
@@ -116,6 +126,8 @@ def set_data_from_ui(self):
116126
for lang in languages_lists:
117127
if is_valid_string(lang[0]):
118128
config_data.server.languages.append(lang[0])
129+
if len(config_data.server.languages) == 0:
130+
config_data.server.languages = None
119131

120132
# limits
121133
default_items = dialog.spinBoxDefault.value()
@@ -124,6 +136,8 @@ def set_data_from_ui(self):
124136
on_exceed = get_enum_value_from_string(
125137
ServerOnExceedEnum, dialog.comboBoxExceed.currentText()
126138
)
139+
if on_exceed == ServerOnExceedEnum.NONE:
140+
on_exceed = None
127141
if default_items or max_items or on_exceed:
128142
config_data.server.limits = ServerLimitsConfig()
129143
config_data.server.limits.default_items = default_items
@@ -169,7 +183,7 @@ def set_data_from_ui(self):
169183
MetadataKeywordTypeEnum, dialog.comboBoxMetadataIdKeywordsType.currentText()
170184
)
171185
config_data.metadata.identification.keywords_type = (
172-
keywords_type if is_valid_string(keywords_type) else None
186+
keywords_type if keywords_type != MetadataKeywordTypeEnum.NONE else None
173187
)
174188

175189
config_data.metadata.identification.terms_of_service = (

ui_widgets/UiSetter.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,25 +72,37 @@ def set_ui_from_data(self):
7272
# gzip
7373
set_combo_box_value_from_data(
7474
combo_box=self.dialog.comboBoxGzip,
75-
value=str(config_data.server.gzip),
75+
value=(
76+
str(config_data.server.gzip.value) if config_data.server.gzip else None
77+
),
7678
)
7779

7880
# pretty print
7981
set_combo_box_value_from_data(
8082
combo_box=self.dialog.comboBoxPretty,
81-
value=str(config_data.server.pretty_print),
83+
value=(
84+
str(config_data.server.pretty_print.value)
85+
if config_data.server.pretty_print
86+
else None
87+
),
8288
)
8389

8490
# admin
8591
set_combo_box_value_from_data(
8692
combo_box=self.dialog.comboBoxAdmin,
87-
value=str(config_data.server.admin),
93+
value=(
94+
str(config_data.server.admin.value)
95+
if config_data.server.admin
96+
else None
97+
),
8898
)
8999

90100
# cors
91101
set_combo_box_value_from_data(
92102
combo_box=self.dialog.comboBoxCors,
93-
value=str(config_data.server.cors),
103+
value=(
104+
str(config_data.server.cors.value) if config_data.server.cors else None
105+
),
94106
)
95107

96108
# mimetype

0 commit comments

Comments
 (0)