From 67d8db15af5b3cf5a68aad1cde702c8c38705952 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 22 Jul 2026 22:38:02 +0200 Subject: [PATCH] feat(relations): delete relations from details --- include/dao/relation_dao.h | 18 ++++++ include/views/main_window.h | 6 ++ include/widgets/workspace.h | 6 ++ src/core/application.c | 116 ++++++++++++++++++++++++++++++++++++ src/dao/relation_dao.c | 47 +++++++++++++++ src/views/main_window.c | 22 +++++++ src/widgets/workspace.c | 32 ++++++++++ tests/test_relation_dao.c | 28 +++++++++ 8 files changed, 275 insertions(+) diff --git a/include/dao/relation_dao.h b/include/dao/relation_dao.h index a805521..afac0ea 100644 --- a/include/dao/relation_dao.h +++ b/include/dao/relation_dao.h @@ -104,6 +104,24 @@ gboolean relation_dao_update( GError **error ); +/** + * @brief Supprime une relation par son UUID. + * + * Les associations dépendantes sont supprimées par les contraintes SQLite. + * Les entités et les preuves ne sont jamais supprimées. + * + * @param relation_dao DAO valide. + * @param identifier UUID de la relation existante. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si la suppression a été exécutée. + */ +gboolean relation_dao_delete( + RelationDao *relation_dao, + const char *identifier, + GError **error +); + /** * @brief Recherche une relation par son UUID. * diff --git a/include/views/main_window.h b/include/views/main_window.h index f7a9b8e..6100184 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -142,6 +142,9 @@ typedef void (*MainWindowEditRelationCallback)( const char *relation_identifier, gpointer user_data ); +/** @brief Callback appelé pour supprimer une relation. */ +typedef void (*MainWindowDeleteRelationCallback)( + const char *relation_identifier, gpointer user_data); /** @brief Callback appelé quand une relation est sélectionnée. */ typedef void (*MainWindowRelationSelectedCallback)( @@ -255,6 +258,9 @@ void main_window_set_edit_relation_callback( MainWindowEditRelationCallback callback, gpointer user_data ); +/** @brief Définit le callback de suppression d'une relation. */ +void main_window_set_delete_relation_callback(MainWindow *main_window, + MainWindowDeleteRelationCallback callback, gpointer user_data); /** @brief Définit le callback de sélection d'une relation. */ void main_window_set_relation_selected_callback( diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index fc91777..78b1d29 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -84,6 +84,9 @@ typedef void (*WorkspaceEditRelationCallback)( const char *relation_identifier, gpointer user_data ); +/** @brief Callback appelé pour supprimer la relation sélectionnée. */ +typedef void (*WorkspaceDeleteRelationCallback)( + const char *relation_identifier, gpointer user_data); /** @brief Callback appelé quand une relation devient sélectionnée. */ typedef void (*WorkspaceRelationSelectedCallback)( @@ -326,6 +329,9 @@ void workspace_set_edit_relation_callback( WorkspaceEditRelationCallback callback, gpointer user_data ); +/** @brief Définit le callback de suppression d'une relation. */ +void workspace_set_delete_relation_callback(Workspace *workspace, + WorkspaceDeleteRelationCallback callback, gpointer user_data); /** * @brief Définit le callback de sélection d'une relation. diff --git a/src/core/application.c b/src/core/application.c index 94324fe..dccf37b 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -4975,6 +4975,120 @@ typedef struct char *relation_identifier; } ApplicationEditRelationContext; +/** @brief Contexte possédé pendant la confirmation de suppression. */ +typedef struct +{ + Application *application; + char *relation_identifier; +} ApplicationDeleteRelationContext; + +/** @brief Libère un contexte de suppression de relation. */ +static void application_delete_relation_context_free( + ApplicationDeleteRelationContext *context) +{ + if (context == NULL) return; + g_free(context->relation_identifier); + g_free(context); +} + +/** @brief Supprime la relation après confirmation explicite. */ +static void application_on_delete_relation_confirmed(gboolean confirmed, + gpointer user_data) +{ + ApplicationDeleteRelationContext *context = user_data; + Application *application = context != NULL ? context->application : NULL; + Database *database = NULL; + RelationDao *relation_dao = NULL; + const InvestigationProject *project = NULL; + GError *error = NULL; + gboolean transaction_active = FALSE; + if (!confirmed || application == NULL || application->session == NULL) + goto cleanup; + database = investigation_session_get_database(application->session); + project = investigation_session_get_project(application->session); + relation_dao = relation_dao_new(database, &error); + if (relation_dao == NULL || !database_transaction_begin(database)) + goto failure; + transaction_active = TRUE; + if (!relation_dao_delete(relation_dao, context->relation_identifier, + &error) || !database_transaction_commit(database)) + goto failure; + transaction_active = FALSE; + main_window_set_status(application->main_window, + "Relation supprimée. Actualisation du graphe…"); + application_start_graph_loading(application, + investigation_project_get_database_path(project)); + goto cleanup; +failure: + if (transaction_active) database_transaction_rollback(database); + application_present_error(application, "Suppression impossible", + error != NULL ? error->message : + "La relation n'a pas pu être supprimée."); +cleanup: + g_clear_error(&error); + relation_dao_free(relation_dao); + application_delete_relation_context_free(context); +} + +/** @brief Prépare la confirmation de suppression d'une relation. */ +static void application_on_delete_relation_requested( + const char *relation_identifier, gpointer user_data) +{ + Application *application = user_data; + Database *database = NULL; + RelationDao *relation_dao = NULL; + EntityDao *entity_dao = NULL; + RelationRecord *relation = NULL; + EntityRecord *source = NULL; + EntityRecord *target = NULL; + ApplicationDeleteRelationContext *context = NULL; + char *message = NULL; + GError *error = NULL; + if (application == NULL || application->session == NULL || + relation_identifier == NULL) return; + database = investigation_session_get_database(application->session); + relation_dao = relation_dao_new(database, &error); + if (relation_dao != NULL) relation = relation_dao_find_by_identifier( + relation_dao, relation_identifier, &error); + entity_dao = entity_dao_new(database, &error); + if (relation != NULL && entity_dao != NULL) + { + source = entity_dao_find_by_identifier(entity_dao, + relation_record_get_source_entity_identifier(relation), &error); + target = entity_dao_find_by_identifier(entity_dao, + relation_record_get_target_entity_identifier(relation), &error); + } + if (relation == NULL || source == NULL || target == NULL) goto failure; + context = g_new0(ApplicationDeleteRelationContext, 1); + context->application = application; + context->relation_identifier = g_strdup(relation_identifier); + message = g_strdup_printf( + "Supprimer définitivement la relation :\n\n%s → %s\n\n" + "Les entités et les preuves resteront dans l’enquête.", + entity_record_get_label(source) != NULL + ? entity_record_get_label(source) : entity_record_get_value(source), + entity_record_get_label(target) != NULL + ? entity_record_get_label(target) : entity_record_get_value(target)); + if (context->relation_identifier == NULL || message == NULL) goto failure; + application_message_dialog_present_confirmation( + main_window_get_window(application->main_window), + APPLICATION_MESSAGE_DIALOG_WARNING, "Supprimer la relation", message, + "Supprimer", application_on_delete_relation_confirmed, context); + context = NULL; + goto cleanup; +failure: + application_delete_relation_context_free(context); context = NULL; + application_present_error(application, "Suppression impossible", + error != NULL ? error->message : + "La relation sélectionnée n'a pas pu être chargée."); +cleanup: + g_free(message); + g_clear_error(&error); + entity_record_free(source); entity_record_free(target); + relation_record_free(relation); + entity_dao_free(entity_dao); relation_dao_free(relation_dao); +} + /** @brief Enregistre la catégorie choisie depuis la fiche personne. */ static void application_on_person_role_changed(const char *entity_identifier, PersonRole role, gpointer user_data) @@ -6728,6 +6842,8 @@ static void application_on_activate( main_window_set_edit_relation_callback(application->main_window, application_on_edit_relation_requested, application); + main_window_set_delete_relation_callback(application->main_window, + application_on_delete_relation_requested, application); main_window_set_relation_selected_callback(application->main_window, application_on_relation_selected, application); main_window_set_person_role_callback(application->main_window, diff --git a/src/dao/relation_dao.c b/src/dao/relation_dao.c index dd0be54..a49ca33 100644 --- a/src/dao/relation_dao.c +++ b/src/dao/relation_dao.c @@ -58,6 +58,10 @@ static const char *const relation_dao_update_sql = "type_relation = ?, label = ?, justification = ?, confiance = ?, " "updated_at = ?, status = ? WHERE id = ?;"; +/** @brief Requête de suppression d'une relation. */ +static const char *const relation_dao_delete_sql = + "DELETE FROM relations WHERE id = ?;"; + /** * @brief Requête de recherche par UUID. */ @@ -1363,6 +1367,49 @@ cleanup: return success; } +gboolean relation_dao_delete(RelationDao *relation_dao, + const char *identifier, GError **error) +{ + DatabaseStatement *statement = NULL; + gboolean success = FALSE; + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + if (relation_dao == NULL || relation_dao->database == NULL || + identifier == NULL || !g_uuid_string_is_valid(identifier)) + { + relation_dao_set_error_literal(error, + RELATION_DAO_ERROR_INVALID_ARGUMENT, + "Les paramètres de suppression de la relation sont invalides."); + return FALSE; + } + statement = database_statement_prepare(relation_dao->database, + relation_dao_delete_sql); + if (statement == NULL) + { + relation_dao_set_database_error(relation_dao, error, + RELATION_DAO_ERROR_PREPARE, + "Impossible de préparer la suppression de la relation"); + goto cleanup; + } + if (!database_statement_bind_text(statement, 1, identifier)) + { + relation_dao_set_database_error(relation_dao, error, + RELATION_DAO_ERROR_BIND, + "Impossible de lier l'identifiant de la relation"); + goto cleanup; + } + if (database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE) + { + relation_dao_set_database_error(relation_dao, error, + RELATION_DAO_ERROR_EXECUTE, + "Impossible de supprimer la relation"); + goto cleanup; + } + success = TRUE; +cleanup: + database_statement_finalize(statement); + return success; +} + RelationRecord *relation_dao_find_by_identifier( RelationDao *relation_dao, const char *identifier, diff --git a/src/views/main_window.c b/src/views/main_window.c index f102f96..3a3b450 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -131,6 +131,8 @@ struct MainWindow MainWindowEditRelationCallback edit_relation_callback; gpointer edit_relation_user_data; + MainWindowDeleteRelationCallback delete_relation_callback; + gpointer delete_relation_user_data; MainWindowPersonRoleCallback person_role_callback; gpointer person_role_user_data; MainWindowPersonConfidenceCallback person_confidence_callback; @@ -532,6 +534,16 @@ static void main_window_on_edit_relation_requested( main_window->edit_relation_user_data); } +/** @brief Relaie la demande de suppression d'une relation. */ +static void main_window_on_delete_relation_requested( + const char *relation_identifier, gpointer user_data) +{ + MainWindow *main_window = user_data; + if (main_window != NULL && main_window->delete_relation_callback != NULL) + main_window->delete_relation_callback(relation_identifier, + main_window->delete_relation_user_data); +} + /** @brief Relaie la catégorisation d'une personne. */ static void main_window_on_person_role_changed(const char *entity_identifier, PersonRole role, gpointer user_data) @@ -903,6 +915,8 @@ MainWindow *main_window_new( workspace_set_edit_relation_callback(main_window->workspace, main_window_on_edit_relation_requested, main_window); + workspace_set_delete_relation_callback(main_window->workspace, + main_window_on_delete_relation_requested, main_window); workspace_set_person_role_callback(main_window->workspace, main_window_on_person_role_changed, main_window); workspace_set_person_confidence_callback(main_window->workspace, @@ -1701,6 +1715,14 @@ void main_window_set_edit_relation_callback(MainWindow *main_window, main_window->edit_relation_user_data = user_data; } +void main_window_set_delete_relation_callback(MainWindow *main_window, + MainWindowDeleteRelationCallback callback, gpointer user_data) +{ + if (main_window == NULL) return; + main_window->delete_relation_callback = callback; + main_window->delete_relation_user_data = user_data; +} + void main_window_set_relation_selected_callback(MainWindow *main_window, MainWindowRelationSelectedCallback callback, gpointer user_data) { diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 2a25fbc..dfea72d 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -69,6 +69,7 @@ struct Workspace GtkWidget *relation_details_summary_label; GtkWidget *relation_evidences_label; GtkWidget *edit_relation_button; + GtkWidget *delete_relation_button; GtkWidget *close_relation_details_button; char *selected_relation_identifier; @@ -131,6 +132,8 @@ struct Workspace add_relation_user_data; WorkspaceEditRelationCallback edit_relation_callback; + WorkspaceDeleteRelationCallback delete_relation_callback; + gpointer delete_relation_user_data; WorkspaceRelationSelectedCallback relation_selected_callback; gpointer relation_selected_user_data; gpointer edit_relation_user_data; @@ -839,6 +842,18 @@ static void workspace_on_edit_relation_clicked(GtkButton *button, workspace->edit_relation_user_data); } +/** @brief Relaie la demande de suppression de la relation affichée. */ +static void workspace_on_delete_relation_clicked(GtkButton *button, + gpointer user_data) +{ + Workspace *workspace = user_data; + (void) button; + if (workspace == NULL || workspace->delete_relation_callback == NULL || + workspace->selected_relation_identifier == NULL) return; + workspace->delete_relation_callback(workspace->selected_relation_identifier, + workspace->delete_relation_user_data); +} + /** @brief Ferme le volet de relation et désélectionne le graphe. */ static void workspace_on_close_relation_details_clicked(GtkButton *button, gpointer user_data) @@ -1712,6 +1727,8 @@ Workspace *workspace_new(void) workspace->relation_evidences_label = gtk_label_new( "Pièces jointes : chargement…"); workspace->edit_relation_button = gtk_button_new_with_label("Modifier"); + workspace->delete_relation_button = gtk_button_new_with_label( + "Supprimer la relation"); workspace->close_relation_details_button = gtk_button_new_from_icon_name("window-close-symbolic"); @@ -1722,6 +1739,7 @@ Workspace *workspace_new(void) workspace->relation_details_summary_label == NULL || workspace->relation_evidences_label == NULL || workspace->edit_relation_button == NULL || + workspace->delete_relation_button == NULL || workspace->close_relation_details_button == NULL) { workspace_free( @@ -1791,8 +1809,14 @@ Workspace *workspace_new(void) workspace->relation_evidences_label); gtk_box_append(GTK_BOX(workspace->relation_details_panel), workspace->edit_relation_button); + gtk_widget_add_css_class(workspace->delete_relation_button, + "destructive-action"); + gtk_box_append(GTK_BOX(workspace->relation_details_panel), + workspace->delete_relation_button); g_signal_connect(workspace->edit_relation_button, "clicked", G_CALLBACK(workspace_on_edit_relation_clicked), workspace); + g_signal_connect(workspace->delete_relation_button, "clicked", + G_CALLBACK(workspace_on_delete_relation_clicked), workspace); g_signal_connect(workspace->close_relation_details_button, "clicked", G_CALLBACK(workspace_on_close_relation_details_clicked), workspace); gtk_widget_set_visible(workspace->relation_details_panel, FALSE); @@ -3245,6 +3269,14 @@ void workspace_set_edit_relation_callback(Workspace *workspace, workspace->edit_relation_user_data = user_data; } +void workspace_set_delete_relation_callback(Workspace *workspace, + WorkspaceDeleteRelationCallback callback, gpointer user_data) +{ + if (workspace == NULL) return; + workspace->delete_relation_callback = callback; + workspace->delete_relation_user_data = user_data; +} + void workspace_set_relation_selected_callback(Workspace *workspace, WorkspaceRelationSelectedCallback callback, gpointer user_data) { diff --git a/tests/test_relation_dao.c b/tests/test_relation_dao.c index 6b9e885..2b7d2d7 100644 --- a/tests/test_relation_dao.c +++ b/tests/test_relation_dao.c @@ -2266,6 +2266,33 @@ static void test_relation_dao_update_existing(void) test_relation_dao_fixture_clear(&fixture); } +/** @brief Vérifie la suppression ciblée sans supprimer les entités. */ +static void test_relation_dao_delete_existing(void) +{ + static const char identifier[] = + "90000000-0000-4000-8000-000000000002"; + TestRelationDaoFixture fixture = test_relation_dao_fixture_create(); + RelationRecord *relation = NULL; + RelationRecord *stored = NULL; + GError *error = NULL; + guint64 count = 0; + test_relation_dao_insert_standard_entities(&fixture); + relation = test_relation_dao_create_relation(identifier, + TEST_ENTITY_A_IDENTIFIER, TEST_ENTITY_B_IDENTIFIER, "uses", + "Relation à supprimer", NULL, 80, "2026-07-20T20:00:00Z", + "2026-07-20T20:00:00Z", RELATION_STATUS_ACTIVE); + test_relation_dao_insert_relation(&fixture, relation); + assert(relation_dao_delete(fixture.relation_dao, identifier, &error)); + assert(error == NULL); + assert(relation_dao_count(fixture.relation_dao, &count, &error)); + assert(count == 0 && error == NULL); + stored = relation_dao_find_by_identifier(fixture.relation_dao, + identifier, &error); + assert(stored == NULL && error == NULL); + relation_record_free(relation); + test_relation_dao_fixture_clear(&fixture); +} + int main( int argc, char **argv @@ -2298,6 +2325,7 @@ int main( test_relation_dao_reflexive_schema_constraint(); test_relation_dao_foreign_key_restrict(); test_relation_dao_update_existing(); + test_relation_dao_delete_existing(); printf( "RelationDao : tous les tests sont valides.\n"