Synchronize user data between local and server

This commit is contained in:
Adrien Ufferte
2025-05-22 17:00:43 +02:00
parent 45f0b68a62
commit ca99391868
12 changed files with 308 additions and 14 deletions
+5 -1
View File
@@ -345,4 +345,8 @@ ADD_STUDENT_POPUP;Souhaitez-vous ajouter un nouvel élève ?;¿Desea añadir un
ADD_DEVICE_POPUP;Souhaitez-vous ajouter un nouvel appareil ? Un élève sera créé automatiquement.;¿Desea añadir un nuevo dispositivo? Se creará un alumno automáticamente.;Gostaria de adicionar um novo dispositivo? Um aluno será criado automaticamente.
DELETE_STUDENT_POPUP;Souhaitez-vous supprimer cet élève ? Cette action est définitive et entrainera la perte de la progression de cet élève.;¿Desea eliminar a este estudiante? Esta acción es definitiva y supondrá la pérdida del progreso de este alumno.;Deseja excluir esse aluno? Essa ação é definitiva e resultará na perda do progresso desse aluno.
DELETE_STUDENT;Supprimer l'élève;Borrar estudiante;Excluir aluno
CHANGE_DEVICE;Changer d'appareil;Cambiar de dispositivo;Trocar de dispositivo
CHANGE_DEVICE;Changer d'appareil;Cambiar de dispositivo;Trocar de dispositivo
SYNCHRONIZE_ACCOUNT;Synchroniser compte;Sincronizar cuenta;Sincronizar conta
SYNC_CHOICE_POPUP;Conflit de données : serveur plus récent. Choisir une version.;Conflicto de datos: el servidor es m‡s reciente. Elige una versi—n.;Conflito de dados: servidor mais recente. Escolha uma vers‹o.
SERVER_PRIORITY;Serveur;Servidor;Servidor
LOCAL_PRIORITY;Locale;Local;Local
1 keys fr es pt_BR
345
346
347
348
349
350
351
352
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -439,6 +439,8 @@ vertical_alignment = 1
visible = false
script = ExtResource("26_pm647")
content_text = "NO_INTERNET_ACCESS"
confirm_text_override = null
cancel_text_override = null
[node name="TextureRect" type="TextureRect" parent="NoInternetPopup"]
top_level = true
+7 -6
View File
@@ -42,7 +42,7 @@ ORDER BY LessonNb")
student_unlock.lesson_GPs = element.GPs
student_unlock.lesson_number = element.LessonNb
if not progression:
Logger.debug("LessonUnlocks: User selected a student with no progression data")
Logger.trace("LessonUnlocks: User selected a student with no progression data")
return
student_unlock.unlocks = progression.unlocks
lesson_container.add_child(student_unlock)
@@ -101,9 +101,10 @@ func _device_selection_refresh() -> void:
func _device_button_pressed(device_id: int) -> void:
device_selection_container.visible = false
UserDataManager.teacher_settings.update_student_device(student, device_id)
await teacher_settings.refresh_devices_tabs()
device = device_id
var resSet: Dictionary = await ServerManager.set_student_data(student, {"device_id": device_id})
if resSet.code == 200:
device_selection_container.visible = false
UserDataManager.teacher_settings.update_student_device(student, device_id)
await teacher_settings.refresh_devices_tabs()
device = device_id
if not resSet.success:
Logger.trace("LessonUnlock: Device was updated locally for the student, but the network update failed.")
+103
View File
@@ -10,6 +10,10 @@ const device_tab_scene: PackedScene = preload("res://sources/menus/settings/devi
@onready var devices_tab_container : TabContainer = %DevicesTabContainer
@onready var lesson_unlocks: LessonUnlocks = $LessonUnlocks
@onready var delete_popup: ConfirmPopup = %DeletePopup
@onready var sync_choice_popup: ConfirmPopup = %SyncChoicePopup
@onready var account_type_option_button: OptionButton = %AccountTypeOptionButton
@onready var education_method_option_button: OptionButton = %EducationMethodOptionButton
@onready var add_device_button: Button = %AddDeviceButton
@onready var add_student_button: Button = %AddStudentButton
@@ -20,6 +24,10 @@ const device_tab_scene: PackedScene = preload("res://sources/menus/settings/devi
var last_device_id: int = -1
var synchronizing: bool = false
var localStringTime: String= ""
func _ready() -> void:
refresh_devices_tabs()
@@ -32,6 +40,26 @@ func _ready() -> void:
OpeningCurtain.open()
lesson_unlocks.teacher_settings = self
account_type_option_button.select(UserDataManager.teacher_settings.account_type)
education_method_option_button.select(UserDataManager.teacher_settings.education_method)
func _on_account_type_option_button_item_selected(index: int) -> void:
if TeacherSettings.AccountType.values().has(index):
UserDataManager.teacher_settings.account_type = index as TeacherSettings.AccountType
UserDataManager.teacher_settings.last_modified = Time.get_datetime_string_from_system(true)
UserDataManager.save_teacher_settings()
else:
Logger.warn("SettingsTeacherSettings: Cannot assign index %d to AccountType" % index)
func _on_education_method_option_button_item_selected(index: int) -> void:
if TeacherSettings.EducationMethod.values().has(index):
UserDataManager.teacher_settings.education_method = index as TeacherSettings.EducationMethod
UserDataManager.teacher_settings.last_modified = Time.get_datetime_string_from_system(true)
UserDataManager.save_teacher_settings()
else:
Logger.warn("SettingsTeacherSettings: Cannot assign index %d to EducationMethod" % index)
func refresh_devices_tabs() -> void:
@@ -68,6 +96,47 @@ func _on_delete_button_pressed() -> void:
delete_popup.show()
func _on_synchronize_button_pressed() -> void:
#synchronizing = false
#await ServerManager.update_user_data(UserDataManager.teacher_settings, "2025-05-23T09:39:47", true)
#return
Logger.trace("SettingsTeacherSettings: Start synchronizing user data.")
if synchronizing:
Logger.trace("SettingsTeacherSettings: User synchronization already started, cancel double-call.")
return
synchronizing = true
var resGetTeacherTimestamp: Dictionary = await ServerManager.get_user_data_timestamp()
if resGetTeacherTimestamp.success:
Logger.trace("SettingsTeacherSettings: Server timestamp for user data = " + str(resGetTeacherTimestamp.body["last_modified"]))
else:
Logger.trace("Cannot get server timestamp for user data. Canceling synchronization.")
synchronizing = false
return
var unixTimeServer: int = Time.get_unix_time_from_datetime_string(resGetTeacherTimestamp.body["last_modified"] as String)
localStringTime = UserDataManager.teacher_settings.last_modified
if localStringTime == "":
var localResult: Dictionary = UserDataManager.get_latest_modification(UserDataManager.get_teacher_folder())
if localResult.error == OK:
localStringTime = Time.get_datetime_string_from_datetime_dict(localResult.modification_date as Dictionary, false)
Logger.trace("SettingsTeacherSettings: Local user file timestamp = " + localStringTime)
else:
Logger.warn("SettingsTeacherSettings: Cannot find modification date for local user data: " + error_string(localResult.error as int))
synchronizing = false
return
var localUnixTime: int = Time.get_unix_time_from_datetime_string(localStringTime)
if localUnixTime == unixTimeServer:
Logger.trace("SettingsTeacherSettings: User data timestamp is the same in local and on server. No synchronization necessary")
elif localUnixTime > unixTimeServer:
_on_sync_choice_local()
else:
Logger.trace("SettingsTeacherSettings: Local user data timestamp is obsolete, we need to choose a priority")
sync_choice_popup.show()
return
synchronizing = false
func _on_delete_popup_accepted() -> void:
var res: Dictionary = await ServerManager.delete_account()
if res.code == 200:
@@ -150,3 +219,37 @@ func update_student_name(student_code: int, student_name: String) -> void:
student_panel.name_label.text = student_name
return
Logger.warn("SettingsTeacherSettings: update_student_name: student not found with code " + str(student_code))
func _on_sync_choice_server() -> void:
Logger.trace("SettingsTeacherSettings: Synchronization priority defined to server")
var resGetUserData: Dictionary = await ServerManager.get_user_data()
if resGetUserData.success:
# {"email": "cvbn@yopmail.com", "account_type": 0, "education_method": 0, "created_at": "2025-05-15T12:27:34Z", "last_modified": "2025-05-22T14:05:42Z"}
if resGetUserData.has("body"):
var resultBody: Dictionary = resGetUserData.body
if resultBody.has("account_type"):
UserDataManager.teacher_settings.account_type = resultBody.account_type
account_type_option_button.select(UserDataManager.teacher_settings.account_type)
else:
Logger.warn("SettingsTeacherSettings: user data received from the server has no account_type")
if resultBody.has("education_method"):
UserDataManager.teacher_settings.education_method = resultBody.education_method
education_method_option_button.select(UserDataManager.teacher_settings.education_method)
else:
Logger.warn("SettingsTeacherSettings: user data received from the server has no education_method")
else:
Logger.warn("SettingsTeacherSettings: user data received from the server has no body")
else:
Logger.warn("SettingsTeacherSettings: Failed to get user data from the server")
synchronizing = false
func _on_sync_choice_local() -> void:
Logger.trace("SettingsTeacherSettings: Synchronization priority defined to local")
var resSetUserTimestamp: Dictionary = await ServerManager.update_user_data(UserDataManager.teacher_settings, localStringTime, true)
if resSetUserTimestamp.success:
Logger.trace("SettingsTeacherSettings: user data successfully sent to the server")
else:
Logger.warn("SettingsTeacherSettings: Failed to send update of user data to the server")
synchronizing = false
+96 -2
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=9 format=3 uid="uid://ci7ueo06f24k3"]
[gd_scene load_steps=10 format=3 uid="uid://ci7ueo06f24k3"]
[ext_resource type="Script" uid="uid://cil0o87dsb7q4" path="res://sources/menus/settings/teacher_settings.gd" id="1_ojivk"]
[ext_resource type="PackedScene" uid="uid://wdjp1sv55q4f" path="res://sources/menus/components/night_sky/night_sky.tscn" id="1_v06h8"]
@@ -7,6 +7,7 @@
[ext_resource type="Texture2D" uid="uid://tcapan0mj64o" path="res://assets/minigames/minigame_ui/graphic/button_back_disabled.png" id="5_e3piv"]
[ext_resource type="PackedScene" uid="uid://bo7rpk4gebh24" path="res://sources/menus/settings/sound_settings_button.tscn" id="6_s8nbl"]
[ext_resource type="PackedScene" uid="uid://bhdh5xfyauss0" path="res://sources/ui/Popup.tscn" id="7_jetb7"]
[ext_resource type="Theme" uid="uid://dqjvrt5nrtwn1" path="res://resources/themes/kalulu_theme.tres" id="8_7h0sa"]
[ext_resource type="PackedScene" uid="uid://qsvbq6ruc44v" path="res://sources/menus/settings/lesson_unlocks.tscn" id="9_mljfd"]
[node name="TeacherSettings" type="Control"]
@@ -56,6 +57,19 @@ theme_override_constants/margin_right = 75
[node name="VolumeButton" parent="InterfaceLeft/Container/VolumeContainer" instance=ExtResource("6_s8nbl")]
layout_mode = 2
[node name="SynchronizeButton" type="Button" parent="InterfaceLeft/Container"]
layout_mode = 2
theme_type_variation = &"FlatButton"
theme_override_font_sizes/font_size = 40
text = "SYNCHRONIZE_ACCOUNT"
[node name="DeletePopup" parent="InterfaceLeft/Container/SynchronizeButton" instance=ExtResource("7_jetb7")]
visible = false
content_text = "Voulez-vous vraiment supprimer votre compte ?
Cette action est irréversible !"
confirm_text_override = null
cancel_text_override = null
[node name="DeleteButton" type="Button" parent="InterfaceLeft/Container"]
layout_mode = 2
theme_type_variation = &"DangerButton"
@@ -67,6 +81,8 @@ unique_name_in_owner = true
visible = false
content_text = "Voulez-vous vraiment supprimer votre compte ?
Cette action est irréversible !"
confirm_text_override = null
cancel_text_override = null
[node name="LogoutButton" type="Button" parent="InterfaceLeft/Container"]
layout_mode = 2
@@ -74,7 +90,7 @@ theme_override_font_sizes/font_size = 40
text = "LOGOUT"
[node name="VBoxContainer" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(2112, 1800)
custom_minimum_size = Vector2(1970, 1800)
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
@@ -84,6 +100,64 @@ offset_left = -632.0
grow_horizontal = 0
grow_vertical = 2
[node name="HSeparator" type="HSeparator" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 50
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="AccountTypeLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
theme = ExtResource("8_7h0sa")
theme_override_font_sizes/font_size = 70
text = "ACCOUNT_TYPE"
[node name="VSeparator" type="VSeparator" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 30
[node name="AccountTypeOptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme = ExtResource("8_7h0sa")
theme_override_font_sizes/font_size = 70
item_count = 2
popup/item_0/text = "TEACHER"
popup/item_0/id = 0
popup/item_1/text = "PARENT"
popup/item_1/id = 1
[node name="VSeparator2" type="VSeparator" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 30
[node name="EducationMethodeLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
theme = ExtResource("8_7h0sa")
theme_override_font_sizes/font_size = 70
text = "EDUCATION_METHOD"
[node name="VSeparator3" type="VSeparator" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 30
[node name="EducationMethodOptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme = ExtResource("8_7h0sa")
theme_override_font_sizes/font_size = 70
item_count = 2
popup/item_0/text = "METHOD_APP_ONLY"
popup/item_0/id = 0
popup/item_1/text = "METHOD_COMPLETE"
popup/item_1/id = 1
[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 50
[node name="DevicesTabContainer" type="TabContainer" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
@@ -104,6 +178,7 @@ alignment = 2
unique_name_in_owner = true
visible = false
layout_mode = 2
theme = ExtResource("8_7h0sa")
theme_override_font_sizes/font_size = 70
text = "SETTINGS_ADD_STUDENT"
@@ -111,6 +186,7 @@ text = "SETTINGS_ADD_STUDENT"
unique_name_in_owner = true
visible = false
layout_mode = 2
theme = ExtResource("8_7h0sa")
theme_override_font_sizes/font_size = 70
text = "SETTINGS_ADD_DEVICE"
@@ -122,21 +198,37 @@ layout_mode = 1
unique_name_in_owner = true
visible = false
content_text = "ADD_STUDENT_POPUP"
confirm_text_override = null
cancel_text_override = null
[node name="AddDevicePopup" parent="." instance=ExtResource("7_jetb7")]
unique_name_in_owner = true
visible = false
content_text = "ADD_DEVICE_POPUP"
confirm_text_override = null
cancel_text_override = null
[node name="DeleteStudentPopup" parent="." instance=ExtResource("7_jetb7")]
unique_name_in_owner = true
visible = false
content_text = "DELETE_STUDENT_POPUP"
confirm_text_override = null
cancel_text_override = null
[node name="SyncChoicePopup" parent="." instance=ExtResource("7_jetb7")]
unique_name_in_owner = true
visible = false
content_text = "SYNC_CHOICE_POPUP"
confirm_text_override = "SERVER_PRIORITY"
cancel_text_override = "LOCAL_PRIORITY"
[connection signal="pressed" from="InterfaceLeft/Container/BackButton" to="." method="_on_back_button_pressed"]
[connection signal="pressed" from="InterfaceLeft/Container/SynchronizeButton" to="." method="_on_synchronize_button_pressed"]
[connection signal="accepted" from="InterfaceLeft/Container/SynchronizeButton/DeletePopup" to="." method="_on_delete_popup_accepted"]
[connection signal="pressed" from="InterfaceLeft/Container/DeleteButton" to="." method="_on_delete_button_pressed"]
[connection signal="accepted" from="InterfaceLeft/Container/DeleteButton/DeletePopup" to="." method="_on_delete_popup_accepted"]
[connection signal="pressed" from="InterfaceLeft/Container/LogoutButton" to="." method="_on_logout_button_pressed"]
[connection signal="item_selected" from="VBoxContainer/HBoxContainer/AccountTypeOptionButton" to="." method="_on_account_type_option_button_item_selected"]
[connection signal="tab_changed" from="VBoxContainer/DevicesTabContainer" to="." method="_on_devices_tab_container_tab_changed"]
[connection signal="pressed" from="VBoxContainer/InterfaceBottom/Container/AddStudentButton" to="." method="_on_add_student_button_pressed"]
[connection signal="pressed" from="VBoxContainer/InterfaceBottom/Container/AddDeviceButton" to="." method="_on_add_device_button_pressed"]
@@ -144,3 +236,5 @@ content_text = "DELETE_STUDENT_POPUP"
[connection signal="accepted" from="AddStudentPopup" to="." method="_on_add_student_popup_accepted"]
[connection signal="accepted" from="AddDevicePopup" to="." method="_on_add_device_popup_accepted"]
[connection signal="accepted" from="DeleteStudentPopup" to="." method="_on_delete_student_popup_accepted"]
[connection signal="accepted" from="SyncChoicePopup" to="." method="_on_sync_choice_server"]
[connection signal="refused" from="SyncChoicePopup" to="." method="_on_sync_choice_local"]
+2
View File
@@ -10,6 +10,8 @@ gradient = SubResource("Gradient_22q4u")
[node name="Popup" type="CanvasLayer"]
script = ExtResource("1_o3fd6")
confirm_text_override = null
cancel_text_override = null
[node name="TextureRect" type="TextureRect" parent="."]
top_level = true
+15
View File
@@ -11,8 +11,15 @@ signal refused
@onready var confirm_button: Button = %ConfirmButton
@onready var cancel_button: Button = %CancelButton
@export var confirm_text_override: String = ""
@export var cancel_text_override: String = ""
func _ready() -> void:
_set_content_text(content_text)
if confirm_text_override != "":
_set_confirm_text(confirm_text_override)
if cancel_text_override != "":
_set_cancel_text(cancel_text_override)
func _set_content_text(p_content_text: String) -> void:
@@ -21,6 +28,14 @@ func _set_content_text(p_content_text: String) -> void:
content_label.text = content_text
func _set_confirm_text(p_content_text: String) -> void:
confirm_button.text = p_content_text
func _set_cancel_text(p_content_text: String) -> void:
cancel_button.text = p_content_text
func _on_confirm_button_pressed() -> void:
accepted.emit()
hide()
+19
View File
@@ -70,8 +70,27 @@ func get_language_pack_url(locale: String) -> Dictionary:
return _response()
func get_user_data_timestamp() -> Dictionary:
await _get_request("get_user_data_timestamp", {})
return _response()
func get_user_data() -> Dictionary:
await _get_request("get_user_data", {})
return _response()
func update_user_data(teacher: TeacherSettings, timestamp: String, force: bool = false) -> Dictionary:
var data: Dictionary = {"account_type": teacher.account_type, "education_method": teacher.education_method ,"timestamp": timestamp}
if force:
data.merge({"force_sync": "YES"})
await _post_json_request("update_user_data", data)
return _response()
# p_student must have a device key
# it can contain a name, level and age key for parents
# TODO POUR ADD / UPDATE / REMOVE STUDENT : RECUPERER LE TIMESTAMP POUR METTRE A JOUR LE USER
func add_student(p_student: Dictionary) -> Dictionary:
await _post_request("add_student", p_student)
return _response()
+59 -5
View File
@@ -45,7 +45,7 @@ func register(register_settings: TeacherSettings) -> bool:
# Save the teacher settings on disk
DirAccess.make_dir_recursive_absolute(get_teacher_folder())
teacher_settings = register_settings
_save_teacher_settings()
save_teacher_settings()
return true
@@ -89,7 +89,7 @@ func login(infos: Dictionary) -> bool:
teacher_settings = load(path) as TeacherSettings
teacher_settings.update_from_dict(infos)
_save_teacher_settings()
save_teacher_settings()
if teacher_settings.students.keys().size() == 1:
set_device_id(teacher_settings.students.keys()[0] as int)
@@ -268,7 +268,7 @@ func _load_teacher_settings() -> void:
_device_settings.device_id = 0
_save_device_settings()
func _save_teacher_settings() -> void:
func save_teacher_settings() -> void:
ResourceSaver.save(teacher_settings, get_teacher_settings_path())
func _delete_inexistants_students_saves() -> void:
@@ -314,7 +314,7 @@ func update_configuration(configuration: Dictionary) -> bool:
if teacher_settings.last_modified != configuration.last_modified:
# Update the teacher resource
teacher_settings.update_from_dict(configuration)
_save_teacher_settings()
save_teacher_settings()
# Check if the device still exists
if not _device_settings.device_id in teacher_settings.students.keys():
@@ -514,6 +514,60 @@ func move_user_device_folder(old_device: String, new_device: String, student_cod
else:
Logger.error("UserDataManager: The folder '%s' cannot be moved because it does no exists in %s." % [old_device, parentDirPath])
return
_save_teacher_settings()
save_teacher_settings()
func get_latest_modification(path: String) -> Dictionary:
var result: Dictionary = {
"error": OK,
"modification_date": null
}
if FileAccess.file_exists(path):
var time: int = FileAccess.get_modified_time(path)
if time > 0:
result.modification_date = Time.get_datetime_dict_from_unix_time(time)
else:
result.error = ERR_CANT_OPEN
return result
if not DirAccess.dir_exists_absolute(path):
result.error = ERR_DOES_NOT_EXIST
return result
var latest_time: int = 0
latest_time = _scan_dir_recursive(path, latest_time)
if latest_time > 0:
result.modification_date = Time.get_datetime_dict_from_unix_time(latest_time)
else:
result.error = ERR_FILE_NOT_FOUND # Aucun fichier trouvé
return result
func _scan_dir_recursive(dir_path: String, latest_time: int) -> int:
var dir: DirAccess = DirAccess.open(dir_path)
if dir == null:
return latest_time
dir.list_dir_begin()
while true:
var dirName: String = dir.get_next()
if dirName == "":
break
if dirName.begins_with("."):
continue
var full_path: String = dir_path.path_join(dirName)
if dir.current_is_dir():
latest_time = _scan_dir_recursive(full_path, latest_time)
else:
var time: int = FileAccess.get_modified_time(full_path)
if time > latest_time:
latest_time = time
dir.list_dir_end()
return latest_time
#endregion