feat(relations): delete relations from details

This commit is contained in:
grayTerminal-sh 2026-07-22 22:38:02 +02:00
parent a6ec67f38d
commit 67d8db15af
8 changed files with 275 additions and 0 deletions

View file

@ -104,6 +104,24 @@ gboolean relation_dao_update(
GError **error 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 é exécutée.
*/
gboolean relation_dao_delete(
RelationDao *relation_dao,
const char *identifier,
GError **error
);
/** /**
* @brief Recherche une relation par son UUID. * @brief Recherche une relation par son UUID.
* *

View file

@ -142,6 +142,9 @@ typedef void (*MainWindowEditRelationCallback)(
const char *relation_identifier, const char *relation_identifier,
gpointer user_data 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. */ /** @brief Callback appelé quand une relation est sélectionnée. */
typedef void (*MainWindowRelationSelectedCallback)( typedef void (*MainWindowRelationSelectedCallback)(
@ -255,6 +258,9 @@ void main_window_set_edit_relation_callback(
MainWindowEditRelationCallback callback, MainWindowEditRelationCallback callback,
gpointer user_data 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. */ /** @brief Définit le callback de sélection d'une relation. */
void main_window_set_relation_selected_callback( void main_window_set_relation_selected_callback(

View file

@ -84,6 +84,9 @@ typedef void (*WorkspaceEditRelationCallback)(
const char *relation_identifier, const char *relation_identifier,
gpointer user_data 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. */ /** @brief Callback appelé quand une relation devient sélectionnée. */
typedef void (*WorkspaceRelationSelectedCallback)( typedef void (*WorkspaceRelationSelectedCallback)(
@ -326,6 +329,9 @@ void workspace_set_edit_relation_callback(
WorkspaceEditRelationCallback callback, WorkspaceEditRelationCallback callback,
gpointer user_data 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. * @brief Définit le callback de sélection d'une relation.

View file

@ -4975,6 +4975,120 @@ typedef struct
char *relation_identifier; char *relation_identifier;
} ApplicationEditRelationContext; } 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 lenquê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. */ /** @brief Enregistre la catégorie choisie depuis la fiche personne. */
static void application_on_person_role_changed(const char *entity_identifier, static void application_on_person_role_changed(const char *entity_identifier,
PersonRole role, gpointer user_data) PersonRole role, gpointer user_data)
@ -6728,6 +6842,8 @@ static void application_on_activate(
main_window_set_edit_relation_callback(application->main_window, main_window_set_edit_relation_callback(application->main_window,
application_on_edit_relation_requested, application); 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, main_window_set_relation_selected_callback(application->main_window,
application_on_relation_selected, application); application_on_relation_selected, application);
main_window_set_person_role_callback(application->main_window, main_window_set_person_role_callback(application->main_window,

View file

@ -58,6 +58,10 @@ static const char *const relation_dao_update_sql =
"type_relation = ?, label = ?, justification = ?, confiance = ?, " "type_relation = ?, label = ?, justification = ?, confiance = ?, "
"updated_at = ?, status = ? WHERE id = ?;"; "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. * @brief Requête de recherche par UUID.
*/ */
@ -1363,6 +1367,49 @@ cleanup:
return success; 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( RelationRecord *relation_dao_find_by_identifier(
RelationDao *relation_dao, RelationDao *relation_dao,
const char *identifier, const char *identifier,

View file

@ -131,6 +131,8 @@ struct MainWindow
MainWindowEditRelationCallback edit_relation_callback; MainWindowEditRelationCallback edit_relation_callback;
gpointer edit_relation_user_data; gpointer edit_relation_user_data;
MainWindowDeleteRelationCallback delete_relation_callback;
gpointer delete_relation_user_data;
MainWindowPersonRoleCallback person_role_callback; MainWindowPersonRoleCallback person_role_callback;
gpointer person_role_user_data; gpointer person_role_user_data;
MainWindowPersonConfidenceCallback person_confidence_callback; MainWindowPersonConfidenceCallback person_confidence_callback;
@ -532,6 +534,16 @@ static void main_window_on_edit_relation_requested(
main_window->edit_relation_user_data); 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. */ /** @brief Relaie la catégorisation d'une personne. */
static void main_window_on_person_role_changed(const char *entity_identifier, static void main_window_on_person_role_changed(const char *entity_identifier,
PersonRole role, gpointer user_data) PersonRole role, gpointer user_data)
@ -903,6 +915,8 @@ MainWindow *main_window_new(
workspace_set_edit_relation_callback(main_window->workspace, workspace_set_edit_relation_callback(main_window->workspace,
main_window_on_edit_relation_requested, main_window); 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, workspace_set_person_role_callback(main_window->workspace,
main_window_on_person_role_changed, main_window); main_window_on_person_role_changed, main_window);
workspace_set_person_confidence_callback(main_window->workspace, 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; 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, void main_window_set_relation_selected_callback(MainWindow *main_window,
MainWindowRelationSelectedCallback callback, gpointer user_data) MainWindowRelationSelectedCallback callback, gpointer user_data)
{ {

View file

@ -69,6 +69,7 @@ struct Workspace
GtkWidget *relation_details_summary_label; GtkWidget *relation_details_summary_label;
GtkWidget *relation_evidences_label; GtkWidget *relation_evidences_label;
GtkWidget *edit_relation_button; GtkWidget *edit_relation_button;
GtkWidget *delete_relation_button;
GtkWidget *close_relation_details_button; GtkWidget *close_relation_details_button;
char *selected_relation_identifier; char *selected_relation_identifier;
@ -131,6 +132,8 @@ struct Workspace
add_relation_user_data; add_relation_user_data;
WorkspaceEditRelationCallback edit_relation_callback; WorkspaceEditRelationCallback edit_relation_callback;
WorkspaceDeleteRelationCallback delete_relation_callback;
gpointer delete_relation_user_data;
WorkspaceRelationSelectedCallback relation_selected_callback; WorkspaceRelationSelectedCallback relation_selected_callback;
gpointer relation_selected_user_data; gpointer relation_selected_user_data;
gpointer edit_relation_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); 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. */ /** @brief Ferme le volet de relation et désélectionne le graphe. */
static void workspace_on_close_relation_details_clicked(GtkButton *button, static void workspace_on_close_relation_details_clicked(GtkButton *button,
gpointer user_data) gpointer user_data)
@ -1712,6 +1727,8 @@ Workspace *workspace_new(void)
workspace->relation_evidences_label = gtk_label_new( workspace->relation_evidences_label = gtk_label_new(
"Pièces jointes : chargement…"); "Pièces jointes : chargement…");
workspace->edit_relation_button = gtk_button_new_with_label("Modifier"); 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 = workspace->close_relation_details_button =
gtk_button_new_from_icon_name("window-close-symbolic"); 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_details_summary_label == NULL ||
workspace->relation_evidences_label == NULL || workspace->relation_evidences_label == NULL ||
workspace->edit_relation_button == NULL || workspace->edit_relation_button == NULL ||
workspace->delete_relation_button == NULL ||
workspace->close_relation_details_button == NULL) workspace->close_relation_details_button == NULL)
{ {
workspace_free( workspace_free(
@ -1791,8 +1809,14 @@ Workspace *workspace_new(void)
workspace->relation_evidences_label); workspace->relation_evidences_label);
gtk_box_append(GTK_BOX(workspace->relation_details_panel), gtk_box_append(GTK_BOX(workspace->relation_details_panel),
workspace->edit_relation_button); 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_signal_connect(workspace->edit_relation_button, "clicked",
G_CALLBACK(workspace_on_edit_relation_clicked), workspace); 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_signal_connect(workspace->close_relation_details_button, "clicked",
G_CALLBACK(workspace_on_close_relation_details_clicked), workspace); G_CALLBACK(workspace_on_close_relation_details_clicked), workspace);
gtk_widget_set_visible(workspace->relation_details_panel, FALSE); 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; 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, void workspace_set_relation_selected_callback(Workspace *workspace,
WorkspaceRelationSelectedCallback callback, gpointer user_data) WorkspaceRelationSelectedCallback callback, gpointer user_data)
{ {

View file

@ -2266,6 +2266,33 @@ static void test_relation_dao_update_existing(void)
test_relation_dao_fixture_clear(&fixture); 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 main(
int argc, int argc,
char **argv char **argv
@ -2298,6 +2325,7 @@ int main(
test_relation_dao_reflexive_schema_constraint(); test_relation_dao_reflexive_schema_constraint();
test_relation_dao_foreign_key_restrict(); test_relation_dao_foreign_key_restrict();
test_relation_dao_update_existing(); test_relation_dao_update_existing();
test_relation_dao_delete_existing();
printf( printf(
"RelationDao : tous les tests sont valides.\n" "RelationDao : tous les tests sont valides.\n"