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) "emergency_surgery": 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))