aboutsummaryrefslogtreecommitdiff
path: root/logic/database.gd
diff options
context:
space:
mode:
Diffstat (limited to 'logic/database.gd')
-rw-r--r--logic/database.gd288
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))
+
+