aboutsummaryrefslogtreecommitdiff
path: root/logic
diff options
context:
space:
mode:
Diffstat (limited to 'logic')
-rw-r--r--logic/database.gd288
-rw-r--r--logic/database_entry.gd74
-rw-r--r--logic/file_picker.gd18
-rw-r--r--logic/menu.gd152
-rw-r--r--logic/stage.gd294
5 files changed, 826 insertions, 0 deletions
diff --git a/logic/database.gd b/logic/database.gd
new file mode 100644
index 0000000..ed80ae7
--- /dev/null
+++ b/logic/database.gd
@@ -0,0 +1,288 @@
+extends TouchItemList
+class_name Database
+
+const DATABASE_FILE_PATH: String = "user://database.json"
+const DATABASE_FILE_VERSION: String = "SL_DB_V1"
+
+var db: Array
+var selected_idx: int
+var staged_idx: int
+
+onready var stage := get_node("/root/main/stage") # as Stage # Commented to avoid cyclic dependencies.
+onready var delete_button := get_node("actions/delete") as Button
+onready var edit_button := get_node("actions/edit") as Button
+onready var add_button := get_node("actions/add") as Button
+onready var popup := get_node("/root/main/popup") as ModalPopup
+onready var dialog := get_node("/root/main/dialog") as Dialog
+
+func _init():
+ selected_idx = -1
+ staged_idx = -1
+
+
+func _ready():
+ self.connect("item_selected", self, "item_selected")
+ self.connect("nothing_selected", self, "clear_selection")
+
+ delete_button.connect("pressed", self, "delete_action")
+ edit_button.connect("pressed", self, "edit_action")
+ add_button.connect("pressed", self, "add_action")
+
+ stage.connect("save", self, "save_stage")
+ stage.connect("discard", self, "discard_stage")
+
+ load_database()
+
+
+func get_entry_view(database_entry: Dictionary) -> String:
+ return "%9s | %4s | %04d-%02d" % [database_entry.process_id, database_entry.surgery_id, database_entry.date_year, database_entry.date_month]
+
+
+func item_selected(index: int):
+ selected_idx = index
+ set_buttons_active(true)
+
+
+func clear_selection():
+ selected_idx = -1
+ unselect_all()
+ set_buttons_active(false)
+
+
+func set_buttons_active(active: bool):
+ (get_node("actions/delete") as Button).disabled = !active
+ (get_node("actions/edit") as Button).disabled = !active
+
+
+func delete_action():
+ if selected_idx < 0:
+ return
+
+ dialog.setup("The entry #%d with process ID '%s' will be deleted from the database." % [selected_idx+1, db[selected_idx].process_id], "Delete", "No")
+ dialog.connect("accepted", self, "delete_action_confirmed")
+ popup.open_popup("Delete entry?", dialog)
+
+
+func delete_action_confirmed():
+ db.remove(selected_idx)
+ self.remove_item(selected_idx)
+ selected_idx = -1
+ save_database()
+ clear_selection()
+
+
+func edit_action():
+ if selected_idx < 0:
+ return
+
+ staged_idx = selected_idx
+ self.visible = false
+ stage.visible = true
+ var staged := (db[staged_idx] as Dictionary).duplicate(true)
+ stage.set_stage(staged, "Entry #%d" % (staged_idx+1))
+
+
+func add_action():
+ self.visible = false
+ stage.visible = true
+ var staged := DatabaseEntry.instance_entry()
+ stage.set_stage(staged, "New entry")
+
+
+func save_stage(entry: Dictionary):
+ if DatabaseEntry.is_valid_entry(entry) == false:
+ printerr("Invalid entry detected.")
+ return
+
+ var next_selected_idx: int
+ if staged_idx >= 0:
+ db[staged_idx] = entry
+ next_selected_idx = staged_idx
+ else:
+ db.append(entry)
+ self.add_item(get_entry_view(entry))
+ next_selected_idx = db.size() - 1
+
+ select(next_selected_idx)
+ emit_signal("item_selected", next_selected_idx) # Calling "select" does not trigger the "item_selected" signal.
+ set_item_text(selected_idx, get_entry_view(db[selected_idx]))
+ ensure_current_is_visible()
+
+ save_database()
+
+ staged_idx = -1
+ self.visible = true
+ grab_focus()
+
+
+func discard_stage():
+ staged_idx = -1
+ self.visible = true
+ grab_focus()
+
+
+func save_database(file_path: String = DATABASE_FILE_PATH):
+ match file_path.get_extension():
+ "json":
+ export_json(file_path, db)
+
+ "csv":
+ export_csv(file_path, db)
+
+ _:
+ printerr("Invalid database file extension '%s', expected 'json' or 'csv'." % file_path.get_file())
+ return
+
+
+static func export_json(file_path: String, data: Array):
+ var database_file := {
+ "version": DATABASE_FILE_VERSION,
+ "database": data,
+ }
+ var indentation_char := "" if file_path == DATABASE_FILE_PATH else "\t"
+ var file_content := JSON.print(database_file, indentation_char)
+
+ var file := File.new()
+ file.open(file_path, File.WRITE)
+ file.store_string(file_content)
+ file.close()
+
+
+static func export_csv(file_path: String, data: Array):
+ var file := File.new()
+ file.open(file_path, File.WRITE)
+ var header := PoolStringArray(DatabaseEntry.ENTRY_PROTOTYPE.keys())
+ file.store_csv_line(header)
+ for it in data:
+ file.store_csv_line(it.values())
+ file.close()
+
+
+func load_database(file_path: String = DATABASE_FILE_PATH):
+ match file_path.get_extension():
+ "json":
+ clear_database()
+ db = import_json(file_path)
+ if file_path != DATABASE_FILE_PATH:
+ sanitize_database(db)
+
+ "csv":
+ clear_database()
+ db = import_csv(file_path)
+
+ _:
+ printerr("Invalid database file extension '%s', expected 'json' or 'csv'." % file_path.get_file())
+ return
+
+ for it in db:
+ # The JSON specification does not define integer or float types, but only a number type.
+ # Therefore, converting a Variant to JSON text will convert all numerical values to float types.
+ # Thus, we cast all integer values once we load them.
+ it["date_year"] = int(it["date_year"])
+ it["date_month"] = int(it["date_month"])
+ it["date_day"] = int(it["date_day"])
+
+ self.add_item(get_entry_view(it))
+
+
+static func sanitize_database(database: Array):
+ var blueprint := DatabaseEntry.ENTRY_PROTOTYPE
+
+ for entry in database:
+ # Delete extra keys.
+ var keys_to_delete: Array
+ for key in entry:
+ if blueprint.has(key) == false:
+ keys_to_delete.append(key)
+ for key in keys_to_delete:
+ entry.erase(key)
+
+ # Fix wrong value types.
+ for key in blueprint:
+ if entry.has(key) == false || typeof(entry[key]) != typeof(blueprint[key]):
+ entry[key] = blueprint[key]
+
+
+static func import_json(file_path: String) -> Array:
+ var result := []
+
+ var file := File.new()
+ var error := file.open(file_path, File.READ_WRITE)
+ if error != OK:
+ printerr("Failed to open database file '%s' (error %d)." % [file_path, error])
+ return result
+ var file_content = file.get_as_text()
+ file.close()
+ var parse_result = JSON.parse(file_content)
+
+ if parse_result.error != OK:
+ printerr("Failed to parse database file '%s' (error %d)." % [file_path, parse_result.error])
+ return result
+
+ if typeof(parse_result.result) != TYPE_DICTIONARY:
+ printerr("Invalid database file type '%s', expected '%s'." % [typeof(parse_result.result), TYPE_DICTIONARY])
+ return result
+
+ if parse_result.result["version"] != DATABASE_FILE_VERSION:
+ printerr("Invalid database file version '%s', expected '%s'." % [parse_result.result["version"], DATABASE_FILE_VERSION])
+ return result
+
+ if typeof(parse_result.result["database"]) != TYPE_ARRAY:
+ printerr("Invalid database content type '%s', expected '%s'." % [typeof(parse_result.result["database"]), TYPE_ARRAY])
+ return result
+
+ result = parse_result.result["database"]
+ return result
+
+
+static func import_csv(file_path: String) -> Array:
+ var result: Array
+ var file := File.new()
+ file.open(file_path, File.READ_WRITE)
+ var headers := file.get_csv_line()
+ while file.get_position() < file.get_len():
+ var csv_entry := file.get_csv_line()
+ var entry = DatabaseEntry.instance_entry()
+ for idx in headers.size():
+ var field_name := headers[idx]
+ var field_value := csv_entry[idx]
+ match field_name:
+ "date":
+ DatabaseEntry.set_entry_date(entry, field_value)
+ "date_year", "date_month", "date_day":
+ entry[field_name] = int(field_value)
+ "is_urgency":
+ entry[field_name] = true if field_value.strip_edges().to_lower() == "true" else false
+ _:
+ if DatabaseEntry.ENTRY_PROTOTYPE.has(field_name):
+ entry[field_name] = field_value
+ if DatabaseEntry.is_valid_entry(entry):
+ result.append(entry)
+ file.close()
+ return result
+
+
+func clear_database():
+ clear_selection()
+ self.clear()
+ db.resize(0)
+
+
+#func DEBUG_create_fake_database():
+# clear_database()
+# for idx in range(100):
+# var today := OS.get_date(true)
+# var date_year = today.year + int(float(idx) / 30.0 / 12)
+# var date_month = 1 + int(float(idx) / 30.0) % 12
+# var date_day = 1 + (idx % 30)
+# var fake_entry = DatabaseEntry.instance_entry({
+# "process_id": "%06d" % idx,
+# "surgery_id": "s%05d" % idx,
+# "date_year": date_year,
+# "date_month": date_month,
+# "date_day": date_day,
+# })
+# db.append(fake_entry)
+# self.add_item(get_entry_view(fake_entry))
+
+
diff --git a/logic/database_entry.gd b/logic/database_entry.gd
new file mode 100644
index 0000000..8b0c51f
--- /dev/null
+++ b/logic/database_entry.gd
@@ -0,0 +1,74 @@
+extends Reference
+class_name DatabaseEntry
+
+const DATE_SEPARATOR: String = "-"
+const DATE_FORMAT: String = "%04d-%02d-%02d"
+const ENTRY_PROTOTYPE: Dictionary = {
+ "process_id": "",
+ "surgery_id": "",
+ "date_year": 1,
+ "date_month": 1,
+ "date_day": 1,
+ "place": "",
+ "anesthesia": "",
+ "first_assistant": "",
+ "type": "",
+ "sub_type": "",
+ "sub_sub_type": "",
+ "pathology": "",
+ "intervention": "",
+ "is_urgency": false,
+ "notes": "",
+}
+
+
+static func instance_entry(params: Dictionary = {}) -> Dictionary:
+ var new_entry := ENTRY_PROTOTYPE.duplicate(true)
+ new_entry.process_id = params.get("process_id", "")
+ new_entry.surgery_id = params.get("surgery_id", "")
+
+ var today = OS.get_date()
+ new_entry.date_year = params.get("date_year", today.year)
+ new_entry.date_month = params.get("date_month", today.month)
+ new_entry.date_day = params.get("date_day", today.day)
+
+ new_entry.place = params.get("place", "")
+ new_entry.anesthesia = params.get("anesthesia", "")
+ new_entry.first_assistant = params.get("first_assistant", "")
+ new_entry.type = params.get("type", "")
+ new_entry.sub_type = params.get("sub_type", "")
+ new_entry.sub_sub_type = params.get("sub_sub_type", "")
+ new_entry.pathology = params.get("pathology", "")
+ new_entry.intervention = params.get("intervention", "")
+ new_entry.is_urgency = params.get("is_urgency", false)
+ new_entry.notes = params.get("notes", "")
+
+ return new_entry
+
+
+static func is_valid_entry(entry: Dictionary) -> bool:
+ var is_valid: bool
+
+ is_valid = entry.has_all(ENTRY_PROTOTYPE.keys()) && ENTRY_PROTOTYPE.keys().size() == entry.keys().size()
+
+ for it in ENTRY_PROTOTYPE.keys():
+ if typeof(ENTRY_PROTOTYPE[it]) != typeof(entry[it]):
+ is_valid = false
+ break
+
+ return is_valid
+
+
+static func get_entry_date(entry: Dictionary) -> String:
+ return DATE_FORMAT % [entry.date_year, entry.date_month, entry.date_day]
+
+
+static func set_entry_date(entry: Dictionary, date: String):
+ date = date.strip_edges().replace(" ", DATE_SEPARATOR).replace("/", DATE_SEPARATOR).replace("\\", DATE_SEPARATOR)
+ var year_month_idx := date.find(DATE_SEPARATOR)
+ var month_day_idx := date.find(DATE_SEPARATOR, year_month_idx + 1)
+ entry.date_year = int(date.substr(0, year_month_idx))
+ entry.date_month = int(date.substr(year_month_idx + 1, month_day_idx - year_month_idx - 1))
+ entry.date_day = int(date.substr(month_day_idx + 1))
+
+
diff --git a/logic/file_picker.gd b/logic/file_picker.gd
new file mode 100644
index 0000000..9715d0d
--- /dev/null
+++ b/logic/file_picker.gd
@@ -0,0 +1,18 @@
+extends FileDialog
+
+export var clear_signals_on_hide := true
+
+
+func _init():
+ self.connect("hide", self, "_clear_signals")
+
+
+func _clear_signals():
+ if clear_signals_on_hide == false:
+ return
+
+ for signal_name in ["file_selected"]:
+ for it in get_signal_connection_list(signal_name):
+ disconnect(it.signal, it.target, it.method)
+
+
diff --git a/logic/menu.gd b/logic/menu.gd
new file mode 100644
index 0000000..4b0b7b3
--- /dev/null
+++ b/logic/menu.gd
@@ -0,0 +1,152 @@
+extends MenuButton
+
+const LOGS_FILE_PATH: String = "user://logs/godot.log"
+const menu_items: Array = [
+ { label = "Import Option Sets", action = "import_option_sets_action" },
+ { label = "Export Option Sets", action = "export_option_sets_action" },
+ { label = "Clear Option Sets", action = "clear_option_sets_action" },
+ { label = "Import Database", action = "import_database_action" },
+ { label = "Export Database", action = "export_database_action" },
+ { label = "Clear Database", action = "clear_database_action" },
+ { label = "Export App Log", action = "export_app_log_action" },
+ { label = "About", action = "about_action" },
+]
+const license_font_b612: String = "res://licenses/font_b612.txt"
+const license_godot: String = "res://licenses/godot.txt"
+
+onready var menu := get_popup() as PopupMenu
+onready var popup := get_node("/root/main/popup") as ModalPopup
+onready var dialog := get_node("/root/main/dialog") as Dialog
+onready var file_picker := get_node("/root/main/file_picker") as FileDialog
+onready var database := get_node("/root/main/database") as Database
+onready var stage := get_node("/root/main/stage") as Stage
+
+
+func _ready():
+ for idx in range(menu_items.size()):
+ menu.add_item(menu_items[idx].label, idx)
+ menu.connect("id_pressed", self, "id_pressed")
+
+
+func id_pressed(id: int):
+ self.call_deferred(menu_items[id].action)
+
+
+func import_option_sets_action():
+ dialog.setup("All option sets from the dropdown menus will be replaced.", "Continue", "No")
+ dialog.connect("accepted", self, "import_option_sets_action_accepted")
+ popup.open_popup("Replace option sets?", dialog)
+
+
+func import_option_sets_action_accepted():
+ file_picker.mode = FileDialog.MODE_OPEN_FILE
+ file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)
+ file_picker.filters = ["*.json", "*.csv"]
+ file_picker.current_file = ""
+ file_picker.connect("file_selected", self, "import_option_sets")
+ file_picker.show_modal(true)
+ file_picker.invalidate()
+
+
+func import_option_sets(file_path: String):
+ stage.load_option_sets(file_path)
+ stage.save_option_sets()
+
+
+func export_option_sets_action():
+ file_picker.mode = FileDialog.MODE_SAVE_FILE
+ file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)
+ file_picker.filters = ["*.json"]
+ file_picker.current_file = ""
+ file_picker.connect("file_selected", stage, "save_option_sets")
+ file_picker.show_modal(true)
+ file_picker.invalidate()
+
+
+func clear_option_sets_action():
+ dialog.setup("All option sets from the dropdown menus will be deleted.", "Delete all", "No")
+ dialog.connect("accepted", self, "clear_option_sets")
+ popup.open_popup("Clear option sets?", dialog)
+
+
+func clear_option_sets():
+ stage.clear_option_sets()
+ stage.save_option_sets()
+
+
+func import_database_action():
+ dialog.setup("All entries from the database will be replaced.", "Continue", "No")
+ dialog.connect("accepted", self, "import_database_action_accepted")
+ popup.open_popup("Replace database?", dialog)
+
+
+func import_database_action_accepted():
+ file_picker.mode = FileDialog.MODE_OPEN_FILE
+ file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)
+ file_picker.filters = ["*.json", "*.csv"]
+ file_picker.current_file = ""
+ file_picker.connect("file_selected", self, "import_database")
+ file_picker.show_modal(true)
+ file_picker.invalidate()
+
+
+func import_database(file_path: String):
+ database.load_database(file_path)
+ database.save_database()
+
+
+func export_database_action():
+ file_picker.mode = FileDialog.MODE_SAVE_FILE
+ file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)
+ file_picker.filters = ["*.csv"]
+ file_picker.current_file = ""
+ file_picker.connect("file_selected", database, "save_database")
+ file_picker.show_modal(true)
+ file_picker.invalidate()
+
+
+func clear_database_action():
+ dialog.setup("All entries from the database will be deleted.", "Delete all", "No")
+ dialog.connect("accepted", self, "clear_database")
+ popup.open_popup("Clear database?", dialog)
+
+
+func clear_database():
+ database.clear_database()
+ database.save_database()
+
+
+func export_app_log_action():
+ file_picker.mode = FileDialog.MODE_SAVE_FILE
+ file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)
+ file_picker.filters = ["*.log"]
+ file_picker.current_file = ""
+ file_picker.connect("file_selected", self, "export_app_log")
+ file_picker.show_modal(true)
+ file_picker.invalidate()
+
+
+func export_app_log(file_path: String):
+ var error : int
+ var file := File.new()
+
+ error = file.open(LOGS_FILE_PATH, File.READ)
+ if error != OK:
+ printerr("Failed to open log file '%s' (error %d)." % [LOGS_FILE_PATH, error])
+ return
+ var file_content = file.get_as_text()
+ file.close()
+
+ error = file.open(file_path, File.WRITE)
+ if error != OK:
+ printerr("Failed to open file '%s' to write log (error %d)." % [file_path, error])
+ return
+ file.store_string(file_content)
+ file.close()
+
+
+func about_action():
+ dialog.setup("Surgery Log\nversion %s" % ProjectSettings.get_setting("global/version"), "", "")
+ popup.open_popup("About", dialog)
+
+
diff --git a/logic/stage.gd b/logic/stage.gd
new file mode 100644
index 0000000..e6d9474
--- /dev/null
+++ b/logic/stage.gd
@@ -0,0 +1,294 @@
+extends TouchVerticalContainer
+class_name Stage
+
+signal save # (database_entry: Dictionary)
+signal discard # ()
+
+const OPTION_SETS_FILE_PATH: String = "user://option_sets.json"
+const OPTION_SETS_FILE_VERSION: String = "SL_OS_V1"
+const OPTION_SETS_NOT_AVAILABLE: String = "--"
+const OPTION_SETS_TREE_STRUCTURE := {
+ "place": null,
+ "anesthesia": null,
+ "first_assistant": null,
+ "type": {
+ "sub_type": {
+ "sub_sub_type": null,
+ "pathology": null,
+ "intervention": null,
+ }
+ }
+}
+
+var staged_entry_hash: int
+var option_sets: Dictionary
+
+onready var title := get_node("controls/title") as Label
+onready var process_id := get_node("controls/process_id") as LineEdit
+onready var surgery_id := get_node("controls/surgery_id") as LineEdit
+onready var date := get_node("controls/date_picker") as DatePicker
+onready var place := get_node("controls/place") as OptionSet
+onready var anesthesia := get_node("controls/anesthesia") as OptionSet
+onready var first_assistant := get_node("controls/first_assistant") as OptionSet
+onready var type := get_node("controls/type") as OptionSet
+onready var sub_type := get_node("controls/sub_type") as OptionSet
+onready var sub_sub_type := get_node("controls/sub_sub_type") as OptionSet
+onready var pathology := get_node("controls/pathology") as OptionSet
+onready var intervention := get_node("controls/intervention") as OptionSet
+onready var is_urgency := get_node("controls/is_urgency") as Button
+onready var notes := get_node("controls/notes") as LineEdit
+onready var save_button := get_node("controls/buttons/save") as Button
+onready var discard_button := get_node("controls/buttons/discard") as Button
+onready var scrollbar := get_v_scrollbar()
+onready var popup := get_node("/root/main/popup") as ModalPopup
+onready var dialog := get_node("/root/main/dialog") as Dialog
+
+
+func _init():
+ exclude_controls = ["date_picker", "save", "discard"]
+ load_option_sets()
+
+
+func _ready():
+ # Fix height of buttons container.
+ (get_node("controls/buttons") as Control).rect_min_size.y = save_button.rect_size.y
+
+ scrollbar.connect("visibility_changed", self, "adjust_layout")
+ save_button.connect("pressed", self, "save_action")
+ discard_button.connect("pressed", self, "discard_action")
+
+ for it in controls.get_children():
+ if it is LineEdit:
+ it.connect("focus_entered", it, "set", ["caret_position", it.max_length])
+ it.connect("focus_exited", it, "deselect")
+
+ # Map option sets buttons.
+ var option_sets_map := {
+ "place": place,
+ "anesthesia": anesthesia,
+ "first_assistant": first_assistant,
+ "type": type,
+ "sub_type": sub_type,
+ "sub_sub_type": sub_sub_type,
+ "pathology": pathology,
+ "intervention": intervention
+ }
+ for key in option_sets_map:
+ var button := option_sets_map[key].get_node("button") as Button
+ button.connect("pressed", self, "show_options", [key])
+
+
+func adjust_layout():
+ var margin_size := scrollbar.rect_size.x
+ self.margin_left = margin_size
+ self.margin_top = margin_size
+ self.margin_bottom = -margin_size
+ self.margin_right = 0.0 if scrollbar.visible else -margin_size
+
+
+func show_options(field: String):
+ var option_set_field := self[field] as OptionSet
+ var options: Array
+
+ match field:
+ "sub_type":
+ options = option_sets.get("type", {}).get(type.text, {}).get(field, {}).keys()
+ "sub_sub_type", "pathology", "intervention":
+ options = option_sets.get("type", {}).get(type.text, {}).get("sub_type", {}).get(sub_type.text, {}).get(field, {}).keys()
+ _:
+ options = option_sets.get(field, {}).keys()
+
+ options.sort()
+ option_set_field.show_options(options)
+
+
+func save_action():
+ self.visible = false
+ var staged_entry := get_stage()
+ gather_option_sets(staged_entry, option_sets)
+ save_option_sets()
+ emit_signal("save", staged_entry)
+
+
+func discard_action():
+ if get_stage().hash() != staged_entry_hash:
+ dialog.setup("Changes made to this entry will be discarded.", "Discard", "No")
+ dialog.connect("accepted", self, "discard_action_confirmed")
+ popup.open_popup("Discard changes?", dialog)
+ else:
+ discard_action_confirmed()
+
+
+func discard_action_confirmed():
+ self.visible = false
+ emit_signal("discard")
+
+
+func set_stage(entry: Dictionary, title: String):
+ self.title.text = title
+ staged_entry_hash = entry.hash()
+ process_id.text = entry.process_id
+ surgery_id.text = entry.surgery_id
+ date.set_date(entry.date_year, entry.date_month, entry.date_day)
+ place.text = entry.place
+ anesthesia.text = entry.anesthesia
+ first_assistant.text = entry.first_assistant
+ type.text = entry.type
+ sub_type.text = entry.sub_type
+ sub_sub_type.text = entry.sub_sub_type
+ pathology.text = entry.pathology
+ intervention.text = entry.intervention
+ is_urgency.pressed = entry.is_urgency
+ notes.text = entry.notes
+ self.scroll_vertical = 0
+
+
+func get_stage() -> Dictionary:
+ var entry := {
+ "process_id": process_id.text,
+ "surgery_id": surgery_id.text,
+ "date_year": date.get_year(),
+ "date_month": date.get_month(),
+ "date_day": date.get_day(),
+ "place": place.text,
+ "anesthesia": anesthesia.text,
+ "first_assistant": first_assistant.text,
+ "type": type.text,
+ "sub_type": sub_type.text,
+ "sub_sub_type": sub_sub_type.text,
+ "pathology": pathology.text,
+ "intervention": intervention.text,
+ "is_urgency": is_urgency.pressed,
+ "notes": notes.text,
+ }
+ return entry
+
+
+static func sanitize_option_sets(entry: Dictionary, blueprint: Dictionary = OPTION_SETS_TREE_STRUCTURE):
+ # Delete extra keys.
+ var keys_to_delete: Array
+ for key in entry:
+ if blueprint.has(key) == false:
+ keys_to_delete.append(key)
+ for key in keys_to_delete:
+ entry.erase(key)
+
+ for key in blueprint:
+ # Add missing keys.
+ if typeof(entry.get(key)) != TYPE_DICTIONARY:
+ entry[key] = {}
+ # Process sub-keys
+ if blueprint[key] != null:
+ for sub_key in entry[key]:
+ if typeof(entry[key][sub_key]) != TYPE_DICTIONARY:
+ entry[key][sub_key] = {}
+ sanitize_option_sets(entry[key][sub_key], blueprint[key])
+
+
+static func gather_option_sets(entry: Dictionary, target: Dictionary, blueprint: Dictionary = OPTION_SETS_TREE_STRUCTURE):
+ for key in blueprint:
+ if target.get(key) == null:
+ target[key] = {}
+
+ var value := (entry[key] as String).strip_edges()
+ if value == "" || value == OPTION_SETS_NOT_AVAILABLE:
+ continue
+
+ if target[key].has(value) == false:
+ target[key][value] = null if blueprint[key] == null else {}
+
+ if blueprint[key] != null:
+ gather_option_sets(entry, target[key][value], blueprint[key])
+
+
+func save_option_sets(file_path: String = OPTION_SETS_FILE_PATH):
+ match file_path.get_extension():
+ "json":
+ export_json(file_path, option_sets)
+
+# "csv":
+# export_csv(file_path, option_sets)
+
+ _:
+ printerr("Invalid option sets file extension '%s', expected 'json'." % file_path.get_file())
+ return
+
+
+static func export_json(file_path: String, data: Dictionary):
+ var option_sets_file := {
+ "version": OPTION_SETS_FILE_VERSION,
+ "option_sets": data,
+ }
+ var indentation_char := "" if file_path == OPTION_SETS_FILE_PATH else "\t"
+ var file_content := JSON.print(option_sets_file, indentation_char)
+
+ var file := File.new()
+ file.open(file_path, File.WRITE)
+ file.store_string(file_content)
+ file.close()
+
+
+#static func export_csv(file_path: String, data: Dictionary):
+# pass
+
+
+func load_option_sets(file_path: String = OPTION_SETS_FILE_PATH):
+ match file_path.get_extension():
+ "json":
+ clear_option_sets()
+ option_sets = import_json(file_path)
+ if file_path != OPTION_SETS_FILE_PATH:
+ sanitize_option_sets(option_sets)
+
+ "csv":
+ clear_option_sets()
+ option_sets = import_csv(file_path)
+
+ _:
+ printerr("Invalid option sets file extension '%s', expected 'json' or 'csv'." % file_path.get_file())
+ return
+
+
+static func import_json(file_path: String) -> Dictionary:
+ var result := {}
+
+ var file := File.new()
+ var error := file.open(file_path, File.READ_WRITE)
+ if error != OK:
+ printerr("Failed to open option sets file '%s' (error %d)." % [file_path, error])
+ return result
+ var file_content = file.get_as_text()
+ file.close()
+ var parse_result = JSON.parse(file_content)
+
+ if parse_result.error != OK:
+ printerr("Failed to parse option sets file '%s' (error %d)." % [file_path, parse_result.error])
+ return result
+
+ if typeof(parse_result.result) != TYPE_DICTIONARY:
+ printerr("Invalid option sets file type '%s', expected '%s'." % [typeof(parse_result.result), TYPE_DICTIONARY])
+ return result
+
+ if parse_result.result["version"] != OPTION_SETS_FILE_VERSION:
+ printerr("Invalid option sets file version '%s', expected '%s'." % [parse_result.result["version"], OPTION_SETS_FILE_VERSION])
+ return result
+
+ if typeof(parse_result.result["option_sets"]) != TYPE_DICTIONARY:
+ printerr("Invalid option sets content type '%s', expected '%s'." % [typeof(parse_result.result["option_sets"]), TYPE_DICTIONARY])
+ return result
+
+ result = parse_result.result["option_sets"]
+ return result
+
+
+static func import_csv(file_path: String) -> Dictionary:
+ var result := {}
+ for it in Database.import_csv(file_path):
+ gather_option_sets(it, result)
+ return result
+
+
+func clear_option_sets():
+ option_sets = {}
+
+