diff options
Diffstat (limited to 'logic/database.gd')
| -rw-r--r-- | logic/database.gd | 288 |
1 files changed, 288 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)) + + |
