Ajouter les relations déplaçables dans le graphe

Affiche les relations comme des nœuds indépendants et persistants.
  Généralise le stockage des positions aux entités et aux relations.
  Finalise la création de relation et le rafraîchissement du graphe.
This commit is contained in:
grayTerminal-sh 2026-07-22 10:15:05 +02:00
parent b1d0ec2a1e
commit bf56b09422
16 changed files with 1286 additions and 325 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
# Build # Build
labfy-investigation labfy-investigation
*.o *.o
*.d
# Executables de tests # Executables de tests
/tests/test_* /tests/test_*

View file

@ -1,4 +1,3 @@
CC = gcc CC = gcc
PKG_CONFIG = pkg-config PKG_CONFIG = pkg-config
@ -11,6 +10,8 @@ CFLAGS = -std=c17 \
-Werror \ -Werror \
-g \ -g \
-Iinclude \ -Iinclude \
-MMD \
-MP \
$(shell $(PKG_CONFIG) --cflags gtk4 sqlite3) $(shell $(PKG_CONFIG) --cflags gtk4 sqlite3)
LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3) LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3)
@ -62,6 +63,8 @@ SRC := $(shell find src -name "*.c")
OBJ := $(SRC:.c=.o) OBJ := $(SRC:.c=.o)
DEP := $(OBJ:.o=.d)
TARGET = labfy-investigation TARGET = labfy-investigation
TEST_NODE = tests/test_investigation_node TEST_NODE = tests/test_investigation_node
@ -649,7 +652,7 @@ run: $(TARGET)
./$(TARGET) ./$(TARGET)
clean: clean:
rm -f $(OBJ) $(TARGET) \ rm -f $(OBJ) $(DEP) $(TARGET) \
$(TEST_NODE) \ $(TEST_NODE) \
$(TEST_TREE_MODEL) \ $(TEST_TREE_MODEL) \
$(TEST_TREE_BUILDER) \ $(TEST_TREE_BUILDER) \
@ -695,4 +698,6 @@ clean:
$(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \
$(TEST_GRAPH_NODE_POSITION_DAO) $(TEST_GRAPH_NODE_POSITION_DAO)
-include $(DEP)
.PHONY: clean run test .PHONY: clean run test

View file

@ -23,16 +23,6 @@ Le projet vise à fournir un environnement local, modulaire et traçable pour or
--- ---
Labfy Investigation est un poste de travail libre dinvestigation numérique et dOSINT, développé en **C17** avec **GTK4**.
Le projet vise à fournir un environnement local, modulaire et traçable pour organiser une enquête, préserver les preuves originales, analyser des données, corréler des entités et produire des rapports exploitables.
> **État du projet : développement actif**
>
> Le logiciel nest pas encore prêt pour un usage opérationnel en production. Les formats internes, linterface et les mécanismes dintégration peuvent encore évoluer.
---
## Objectifs ## Objectifs
Labfy Investigation doit permettre de : Labfy Investigation doit permettre de :

View file

@ -57,3 +57,45 @@ CREATE TABLE IF NOT EXISTS graph_node_positions
length(trim(updated_at)) > 0 length(trim(updated_at)) > 0
) )
); );
/*
* Disposition générique du graphe.
*
* Contrairement à graph_node_positions, cette table accepte aussi les UUID
* des relations. L'ancienne table reste présente pour assurer la compatibilité
* avec les enquêtes créées avant l'introduction des nœuds de relation.
*/
CREATE TABLE IF NOT EXISTS graph_layout_positions
(
node_id TEXT PRIMARY KEY,
x REAL NOT NULL,
y REAL NOT NULL,
updated_at TEXT NOT NULL,
CHECK (length(trim(node_id)) > 0),
CHECK (length(updated_at) = 20)
);
/* Migration idempotente des positions d'entités déjà enregistrées. */
INSERT OR IGNORE INTO graph_layout_positions(node_id, x, y, updated_at)
SELECT entity_id, x, y, updated_at
FROM graph_node_positions;
/* Évite qu'une réinitialisation future ne réimporte des coordonnées obsolètes. */
DELETE FROM graph_node_positions;
/* Une clé étrangère polymorphe n'existe pas dans SQLite : ces triggers
* suppriment donc les positions devenues orphelines. */
CREATE TRIGGER IF NOT EXISTS graph_layout_positions_delete_entity
AFTER DELETE ON entites
FOR EACH ROW
BEGIN
DELETE FROM graph_layout_positions WHERE node_id = OLD.id;
END;
CREATE TRIGGER IF NOT EXISTS graph_layout_positions_delete_relation
AFTER DELETE ON relations
FOR EACH ROW
BEGIN
DELETE FROM graph_layout_positions WHERE node_id = OLD.id;
END;

View file

@ -2181,6 +2181,30 @@ des fichiers ou des notes.
--- ---
## 11.5 Disposition générique du graphe
La disposition visuelle du graphe est un état de présentation et non une
donnée métier. Elle est stockée dans `graph_layout_positions`, séparément des
tables `entites` et `relations`.
Chaque ligne associe un UUID de nœud à des coordonnées logiques et à une date
UTC de mise à jour. Un nœud peut représenter une entité ou une relation. SQLite
ne proposant pas de clé étrangère polymorphe, l'intégrité de ce stockage est
assurée par deux triggers qui suppriment la position correspondante lors de la
suppression physique d'une entité ou d'une relation.
La table historique `graph_node_positions` ne référençait que `entites(id)`.
À l'ouverture d'une enquête, ses lignes sont copiées de manière idempotente
vers `graph_layout_positions`, puis retirées de la table historique afin
qu'une réinitialisation volontaire de la disposition ne puisse pas restaurer
des coordonnées obsolètes.
Cette stratégie préserve les positions existantes tout en permettant aux
nœuds de relation de suivre exactement le même cycle de chargement,
d'enregistrement et de réinitialisation que les nœuds d'entité.
---
# 12. Conclusion # 12. Conclusion
La base de données de Labfy Investigation constitue bien davantage qu'un simple La base de données de Labfy Investigation constitue bien davantage qu'un simple

View file

@ -91,12 +91,12 @@ GPtrArray *graph_node_position_dao_list_all(
); );
/** /**
* @brief Insère ou met à jour la position d'une entité. * @brief Insère ou met à jour la position d'un nœud.
* *
* La date updated_at est générée en UTC par le DAO. * La date updated_at est générée en UTC par le DAO.
* *
* @param position_dao DAO valide. * @param position_dao DAO valide.
* @param entity_identifier UUID de l'entité. * @param node_identifier UUID du nœud (entité ou relation).
* @param x Coordonnée horizontale logique. * @param x Coordonnée horizontale logique.
* @param y Coordonnée verticale logique. * @param y Coordonnée verticale logique.
* @param error Adresse recevant une éventuelle erreur. * @param error Adresse recevant une éventuelle erreur.
@ -105,26 +105,26 @@ GPtrArray *graph_node_position_dao_list_all(
*/ */
gboolean graph_node_position_dao_upsert( gboolean graph_node_position_dao_upsert(
GraphNodePositionDao *position_dao, GraphNodePositionDao *position_dao,
const char *entity_identifier, const char *node_identifier,
double x, double x,
double y, double y,
GError **error GError **error
); );
/** /**
* @brief Supprime la position persistée d'une entité. * @brief Supprime la position persistée d'un nœud.
* *
* L'absence de position n'est pas une erreur. * L'absence de position n'est pas une erreur.
* *
* @param position_dao DAO valide. * @param position_dao DAO valide.
* @param entity_identifier UUID de l'entité. * @param node_identifier UUID du nœud (entité ou relation).
* @param error Adresse recevant une éventuelle erreur. * @param error Adresse recevant une éventuelle erreur.
* *
* @return TRUE si la requête réussit, sinon FALSE. * @return TRUE si la requête réussit, sinon FALSE.
*/ */
gboolean graph_node_position_dao_delete( gboolean graph_node_position_dao_delete(
GraphNodePositionDao *position_dao, GraphNodePositionDao *position_dao,
const char *entity_identifier, const char *node_identifier,
GError **error GError **error
); );

View file

@ -42,11 +42,11 @@ GQuark graph_node_position_error_quark(void);
* *
* Toutes les chaînes sont copiées. * Toutes les chaînes sont copiées.
* *
* entity_identifier doit être un UUID valide. * node_identifier doit être un UUID valide.
* x et y doivent être des nombres finis. * x et y doivent être des nombres finis.
* updated_at doit respecter le format UTC YYYY-MM-DDTHH:MM:SSZ. * updated_at doit respecter le format UTC YYYY-MM-DDTHH:MM:SSZ.
* *
* @param entity_identifier UUID de l'entité. * @param node_identifier UUID du nœud (entité ou relation).
* @param x Coordonnée horizontale logique. * @param x Coordonnée horizontale logique.
* @param y Coordonnée verticale logique. * @param y Coordonnée verticale logique.
* @param updated_at Date UTC de dernière modification. * @param updated_at Date UTC de dernière modification.
@ -55,7 +55,7 @@ GQuark graph_node_position_error_quark(void);
* @return Nouvelle position, ou NULL lorsque les données sont invalides. * @return Nouvelle position, ou NULL lorsque les données sont invalides.
*/ */
GraphNodePosition *graph_node_position_new( GraphNodePosition *graph_node_position_new(
const char *entity_identifier, const char *node_identifier,
double x, double x,
double y, double y,
const char *updated_at, const char *updated_at,
@ -74,7 +74,7 @@ void graph_node_position_free(
); );
/** /**
* @brief Retourne l'UUID de l'entité. * @brief Retourne l'UUID du nœud (entité ou relation).
* *
* La chaîne retournée appartient au modèle. * La chaîne retournée appartient au modèle.
* *
@ -82,7 +82,7 @@ void graph_node_position_free(
* *
* @return UUID emprunté, ou NULL. * @return UUID emprunté, ou NULL.
*/ */
const char *graph_node_position_get_entity_identifier( const char *graph_node_position_get_node_identifier(
const GraphNodePosition *position const GraphNodePosition *position
); );

View file

@ -47,16 +47,16 @@ typedef void (*InvestigationGraphViewSelectionCallback)(
* @brief Callback appelé après le déplacement effectif d'un nœud. * @brief Callback appelé après le déplacement effectif d'un nœud.
* *
* Les coordonnées sont exprimées dans l'espace logique du graphe. * Les coordonnées sont exprimées dans l'espace logique du graphe.
* entity_identifier est emprunté au modèle et reste valable uniquement * node_identifier est emprunté au modèle et reste valable uniquement
* pendant l'appel. * pendant l'appel.
* *
* @param entity_identifier UUID de l'entité déplacée. * @param node_identifier UUID du nœud déplacé (entité ou relation).
* @param x Coordonnée horizontale logique du coin supérieur gauche. * @param x Coordonnée horizontale logique du coin supérieur gauche.
* @param y Coordonnée verticale logique du coin supérieur gauche. * @param y Coordonnée verticale logique du coin supérieur gauche.
* @param user_data Données empruntées fournies par l'appelant. * @param user_data Données empruntées fournies par l'appelant.
*/ */
typedef void (*InvestigationGraphViewNodeMovedCallback)( typedef void (*InvestigationGraphViewNodeMovedCallback)(
const char *entity_identifier, const char *node_identifier,
double x, double x,
double y, double y,
gpointer user_data gpointer user_data

View file

@ -35,6 +35,7 @@
#include "widgets/evidence_category_model.h" #include "widgets/evidence_category_model.h"
#include "core/evidence_integrity_task.h" #include "core/evidence_integrity_task.h"
#include "core/evidence_integrity_verifier.h" #include "core/evidence_integrity_verifier.h"
#include "core/relation_service.h"
#include "database/database.h" #include "database/database.h"
#include <gtk/gtk.h> #include <gtk/gtk.h>
@ -3341,6 +3342,21 @@ static void application_on_create_relation_completed(
Application *application = Application *application =
user_data; user_data;
const InvestigationProject *project =
NULL;
Database *database =
NULL;
RelationService *relation_service =
NULL;
RelationRecord *relation_record =
NULL;
GDateTime *current_date_time =
NULL;
const char *source_identifier = const char *source_identifier =
NULL; NULL;
@ -3350,9 +3366,21 @@ static void application_on_create_relation_completed(
const char *relation_type = const char *relation_type =
NULL; NULL;
char *status_message = const char *database_path =
NULL; NULL;
char *relation_identifier =
NULL;
char *timestamp =
NULL;
GError *error =
NULL;
gboolean relation_created =
FALSE;
if (application == NULL || if (application == NULL ||
application->main_window == NULL) application->main_window == NULL)
{ {
@ -3373,6 +3401,17 @@ static void application_on_create_relation_completed(
return; return;
} }
if (application->session == NULL)
{
application_present_error(
application,
"Ajout de relation impossible",
"Aucune enquête n'est actuellement ouverte."
);
goto cleanup;
}
source_identifier = source_identifier =
create_relation_dialog_result_get_source_identifier( create_relation_dialog_result_get_source_identifier(
result result
@ -3388,41 +3427,225 @@ static void application_on_create_relation_completed(
result result
); );
g_message( database =
"Relation préparée : %s -> %s (%s), confiance %d %%.", investigation_session_get_database(
source_identifier != NULL application->session
? source_identifier
: "(source absente)",
target_identifier != NULL
? target_identifier
: "(cible absente)",
relation_type != NULL
? relation_type
: "(type absent)",
create_relation_dialog_result_get_confidence(
result
)
); );
status_message = project =
g_strdup_printf( investigation_session_get_project(
"Relation préparée : %s. " application->session
"L'enregistrement SQLite sera branché à l'étape suivante.", );
relation_type != NULL &&
relation_type[0] != '\0' if (database == NULL ||
? relation_type project == NULL)
: "(type inconnu)" {
application_present_error(
application,
"Ajout de relation impossible",
"La session d'enquête est invalide."
);
goto cleanup;
}
database_path =
investigation_project_get_database_path(
project
);
if (database_path == NULL ||
database_path[0] == '\0')
{
application_present_error(
application,
"Ajout de relation impossible",
"Le chemin de la base SQLite est invalide."
);
goto cleanup;
}
relation_identifier =
g_uuid_string_random();
current_date_time =
g_date_time_new_now_utc();
if (current_date_time != NULL)
{
timestamp =
g_date_time_format(
current_date_time,
"%Y-%m-%dT%H:%M:%SZ"
);
}
if (relation_identifier == NULL ||
timestamp == NULL)
{
application_present_error(
application,
"Ajout de relation impossible",
"Impossible de préparer l'identifiant ou la date "
"de la nouvelle relation."
);
goto cleanup;
}
relation_record =
relation_record_new(
relation_identifier,
source_identifier,
target_identifier,
relation_type,
create_relation_dialog_result_get_label(
result
),
create_relation_dialog_result_get_justification(
result
),
create_relation_dialog_result_get_confidence(
result
),
timestamp,
timestamp,
RELATION_STATUS_ACTIVE,
&error
);
if (relation_record == NULL)
{
application_present_error(
application,
"Relation invalide",
error != NULL
? error->message
: "Les informations de la relation sont invalides."
);
g_clear_error(
&error
);
goto cleanup;
}
relation_service =
relation_service_new(
database,
&error
);
if (relation_service == NULL)
{
application_present_error(
application,
"Ajout de relation impossible",
error != NULL
? error->message
: "Impossible de préparer le service des relations."
);
g_clear_error(
&error
);
goto cleanup;
}
/*
* Aucune preuve n'est associée pendant ce premier parcours.
* Le service accepte donc un tableau NULL.
*/
if (!relation_service_create(
relation_service,
relation_record,
NULL,
&error
))
{
g_warning(
"Impossible d'enregistrer la relation '%s' : %s",
relation_identifier,
error != NULL
? error->message
: "erreur inconnue"
);
application_present_error(
application,
"Enregistrement de la relation impossible",
error != NULL
? error->message
: "La relation n'a pas pu être enregistrée."
);
g_clear_error(
&error
);
goto cleanup;
}
relation_created =
TRUE;
g_message(
"Relation enregistrée : %s -> %s (%s), identifiant %s.",
source_identifier,
target_identifier,
relation_type,
relation_identifier
); );
main_window_set_status( main_window_set_status(
application->main_window, application->main_window,
status_message != NULL "Relation enregistrée. Actualisation du graphe…"
? status_message );
: "Relation préparée."
/*
* Le graphe n'est rechargé qu'après le COMMIT réussi.
* En cas d'échec SQLite, l'affichage courant reste intact.
*/
application_start_graph_loading(
application,
database_path
);
cleanup:
if (!relation_created &&
application->main_window != NULL)
{
main_window_set_status(
application->main_window,
"La relation n'a pas été enregistrée."
);
}
relation_service_free(
relation_service
);
relation_record_free(
relation_record
);
if (current_date_time != NULL)
{
g_date_time_unref(
current_date_time
);
}
g_free(
timestamp
); );
g_free( g_free(
status_message relation_identifier
); );
create_relation_dialog_result_free( create_relation_dialog_result_free(

View file

@ -946,7 +946,7 @@ static gboolean investigation_graph_load_task_worker(
if (!investigation_graph_layout_set_position( if (!investigation_graph_layout_set_position(
graph_layout, graph_layout,
graph_node_position_get_entity_identifier( graph_node_position_get_node_identifier(
position position
), ),
graph_node_position_get_x( graph_node_position_get_x(

View file

@ -27,20 +27,20 @@ struct GraphNodePositionDao
*/ */
static const char *const graph_node_position_dao_list_all_sql = static const char *const graph_node_position_dao_list_all_sql =
"SELECT " "SELECT "
" entity_id," " node_id,"
" x," " x,"
" y," " y,"
" updated_at " " updated_at "
"FROM graph_node_positions " "FROM graph_layout_positions "
"ORDER BY entity_id ASC;"; "ORDER BY node_id ASC;";
/** /**
* @brief Requête d'insertion ou de mise à jour d'une position. * @brief Requête d'insertion ou de mise à jour d'une position.
*/ */
static const char *const graph_node_position_dao_upsert_sql = static const char *const graph_node_position_dao_upsert_sql =
"INSERT INTO graph_node_positions" "INSERT INTO graph_layout_positions"
"(" "("
" entity_id," " node_id,"
" x," " x,"
" y," " y,"
" updated_at" " updated_at"
@ -52,7 +52,7 @@ static const char *const graph_node_position_dao_upsert_sql =
" ?," " ?,"
" ?" " ?"
")" ")"
"ON CONFLICT(entity_id)" "ON CONFLICT(node_id)"
"DO UPDATE SET " "DO UPDATE SET "
" x = excluded.x," " x = excluded.x,"
" y = excluded.y," " y = excluded.y,"
@ -62,14 +62,14 @@ static const char *const graph_node_position_dao_upsert_sql =
* @brief Requête de suppression d'une position. * @brief Requête de suppression d'une position.
*/ */
static const char *const graph_node_position_dao_delete_sql = static const char *const graph_node_position_dao_delete_sql =
"DELETE FROM graph_node_positions " "DELETE FROM graph_layout_positions "
"WHERE entity_id = ?;"; "WHERE node_id = ?;";
/** /**
* @brief Requête de suppression de toutes les positions. * @brief Requête de suppression de toutes les positions.
*/ */
static const char *const graph_node_position_dao_delete_all_sql = static const char *const graph_node_position_dao_delete_all_sql =
"DELETE FROM graph_node_positions;"; "DELETE FROM graph_layout_positions;";
/** /**
* @brief Enregistre une erreur littérale. * @brief Enregistre une erreur littérale.
@ -202,7 +202,7 @@ graph_node_position_dao_read_current_record(
GraphNodePosition *position = GraphNodePosition *position =
NULL; NULL;
char *entity_identifier = char *node_identifier =
NULL; NULL;
char *updated_at = char *updated_at =
@ -233,7 +233,7 @@ graph_node_position_dao_read_current_record(
if (!database_statement_column_text( if (!database_statement_column_text(
statement, statement,
0, 0,
&entity_identifier &node_identifier
) || ) ||
!database_statement_column_double( !database_statement_column_double(
statement, statement,
@ -262,7 +262,7 @@ graph_node_position_dao_read_current_record(
position = position =
graph_node_position_new( graph_node_position_new(
entity_identifier, node_identifier,
x, x,
y, y,
updated_at, updated_at,
@ -302,7 +302,7 @@ cleanup:
); );
g_free( g_free(
entity_identifier node_identifier
); );
return position; return position;
@ -576,7 +576,7 @@ error:
gboolean graph_node_position_dao_upsert( gboolean graph_node_position_dao_upsert(
GraphNodePositionDao *position_dao, GraphNodePositionDao *position_dao,
const char *entity_identifier, const char *node_identifier,
double x, double x,
double y, double y,
GError **error GError **error
@ -598,9 +598,9 @@ gboolean graph_node_position_dao_upsert(
if (position_dao == NULL || if (position_dao == NULL ||
position_dao->database == NULL || position_dao->database == NULL ||
entity_identifier == NULL || node_identifier == NULL ||
!g_uuid_string_is_valid( !g_uuid_string_is_valid(
entity_identifier node_identifier
) || ) ||
!isfinite(x) || !isfinite(x) ||
!isfinite(y)) !isfinite(y))
@ -649,7 +649,7 @@ gboolean graph_node_position_dao_upsert(
if (!database_statement_bind_text( if (!database_statement_bind_text(
statement, statement,
1, 1,
entity_identifier node_identifier
) || ) ||
!database_statement_bind_double( !database_statement_bind_double(
statement, statement,
@ -707,7 +707,7 @@ cleanup:
gboolean graph_node_position_dao_delete( gboolean graph_node_position_dao_delete(
GraphNodePositionDao *position_dao, GraphNodePositionDao *position_dao,
const char *entity_identifier, const char *node_identifier,
GError **error GError **error
) )
{ {
@ -724,15 +724,15 @@ gboolean graph_node_position_dao_delete(
if (position_dao == NULL || if (position_dao == NULL ||
position_dao->database == NULL || position_dao->database == NULL ||
entity_identifier == NULL || node_identifier == NULL ||
!g_uuid_string_is_valid( !g_uuid_string_is_valid(
entity_identifier node_identifier
)) ))
{ {
graph_node_position_dao_set_error_literal( graph_node_position_dao_set_error_literal(
error, error,
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT, GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
"L'identifiant de l'entité est invalide." "L'identifiant du nœud est invalide."
); );
return FALSE; return FALSE;
@ -759,14 +759,14 @@ gboolean graph_node_position_dao_delete(
if (!database_statement_bind_text( if (!database_statement_bind_text(
statement, statement,
1, 1,
entity_identifier node_identifier
)) ))
{ {
graph_node_position_dao_set_database_error( graph_node_position_dao_set_database_error(
position_dao, position_dao,
error, error,
GRAPH_NODE_POSITION_DAO_ERROR_BIND, GRAPH_NODE_POSITION_DAO_ERROR_BIND,
"Impossible de lier l'identifiant de l'entité" "Impossible de lier l'identifiant du nœud"
); );
goto cleanup; goto cleanup;

View file

@ -14,7 +14,7 @@
*/ */
struct GraphNodePosition struct GraphNodePosition
{ {
char *entity_identifier; char *node_identifier;
double x; double x;
double y; double y;
@ -154,7 +154,7 @@ GQuark graph_node_position_error_quark(void)
} }
GraphNodePosition *graph_node_position_new( GraphNodePosition *graph_node_position_new(
const char *entity_identifier, const char *node_identifier,
double x, double x,
double y, double y,
const char *updated_at, const char *updated_at,
@ -164,7 +164,7 @@ GraphNodePosition *graph_node_position_new(
GraphNodePosition *position = GraphNodePosition *position =
NULL; NULL;
char *entity_identifier_copy = char *node_identifier_copy =
NULL; NULL;
char *updated_at_copy = char *updated_at_copy =
@ -175,22 +175,22 @@ GraphNodePosition *graph_node_position_new(
NULL NULL
); );
entity_identifier_copy = node_identifier_copy =
graph_node_position_duplicate_trimmed( graph_node_position_duplicate_trimmed(
entity_identifier node_identifier
); );
if (entity_identifier_copy == NULL || if (node_identifier_copy == NULL ||
entity_identifier_copy[0] == '\0' || node_identifier_copy[0] == '\0' ||
!g_uuid_string_is_valid( !g_uuid_string_is_valid(
entity_identifier_copy node_identifier_copy
)) ))
{ {
g_set_error_literal( g_set_error_literal(
error, error,
GRAPH_NODE_POSITION_ERROR, GRAPH_NODE_POSITION_ERROR,
GRAPH_NODE_POSITION_ERROR_INVALID_IDENTIFIER, GRAPH_NODE_POSITION_ERROR_INVALID_IDENTIFIER,
"L'identifiant de l'entité n'est pas un UUID valide." "L'identifiant du nœud n'est pas un UUID valide."
); );
goto cleanup; goto cleanup;
@ -246,8 +246,8 @@ GraphNodePosition *graph_node_position_new(
goto cleanup; goto cleanup;
} }
position->entity_identifier = position->node_identifier =
entity_identifier_copy; node_identifier_copy;
position->x = position->x =
x; x;
@ -258,7 +258,7 @@ GraphNodePosition *graph_node_position_new(
position->updated_at = position->updated_at =
updated_at_copy; updated_at_copy;
entity_identifier_copy = node_identifier_copy =
NULL; NULL;
updated_at_copy = updated_at_copy =
@ -271,7 +271,7 @@ cleanup:
); );
g_free( g_free(
entity_identifier_copy node_identifier_copy
); );
return position; return position;
@ -291,7 +291,7 @@ void graph_node_position_free(
); );
g_free( g_free(
position->entity_identifier position->node_identifier
); );
g_free( g_free(
@ -299,12 +299,12 @@ void graph_node_position_free(
); );
} }
const char *graph_node_position_get_entity_identifier( const char *graph_node_position_get_node_identifier(
const GraphNodePosition *position const GraphNodePosition *position
) )
{ {
return position != NULL return position != NULL
? position->entity_identifier ? position->node_identifier
: NULL; : NULL;
} }

View file

@ -8,7 +8,7 @@
#include <math.h> #include <math.h>
/** /**
* @brief Coordonnées privées associées à une entité. * @brief Coordonnées privées associées à un nœud.
*/ */
typedef struct typedef struct
{ {
@ -22,7 +22,7 @@ typedef struct
*/ */
struct InvestigationGraphLayout struct InvestigationGraphLayout
{ {
GHashTable *positions_by_entity_identifier; GHashTable *positions_by_node_identifier;
}; };
/** /**
@ -48,16 +48,16 @@ static void investigation_graph_layout_set_error_literal(
} }
/** /**
* @brief Vérifie un identifiant d'entité. * @brief Vérifie un identifiant de nœud.
*/ */
static gboolean investigation_graph_layout_identifier_is_valid( static gboolean investigation_graph_layout_identifier_is_valid(
const char *entity_identifier const char *node_identifier
) )
{ {
return entity_identifier != NULL && return node_identifier != NULL &&
entity_identifier[0] != '\0' && node_identifier[0] != '\0' &&
g_uuid_string_is_valid( g_uuid_string_is_valid(
entity_identifier node_identifier
); );
} }
@ -84,7 +84,7 @@ InvestigationGraphLayout *investigation_graph_layout_new(void)
return NULL; return NULL;
} }
layout->positions_by_entity_identifier = layout->positions_by_node_identifier =
g_hash_table_new_full( g_hash_table_new_full(
g_str_hash, g_str_hash,
g_str_equal, g_str_equal,
@ -92,7 +92,7 @@ InvestigationGraphLayout *investigation_graph_layout_new(void)
g_free g_free
); );
if (layout->positions_by_entity_identifier == NULL) if (layout->positions_by_node_identifier == NULL)
{ {
investigation_graph_layout_free( investigation_graph_layout_free(
layout layout
@ -106,7 +106,7 @@ InvestigationGraphLayout *investigation_graph_layout_new(void)
gboolean investigation_graph_layout_set_position( gboolean investigation_graph_layout_set_position(
InvestigationGraphLayout *layout, InvestigationGraphLayout *layout,
const char *entity_identifier, const char *node_identifier,
double x, double x,
double y, double y,
GError **error GError **error
@ -115,7 +115,7 @@ gboolean investigation_graph_layout_set_position(
InvestigationGraphLayoutPosition *position = InvestigationGraphLayoutPosition *position =
NULL; NULL;
char *entity_identifier_copy = char *node_identifier_copy =
NULL; NULL;
g_return_val_if_fail( g_return_val_if_fail(
@ -124,7 +124,7 @@ gboolean investigation_graph_layout_set_position(
); );
if (layout == NULL || if (layout == NULL ||
layout->positions_by_entity_identifier == NULL) layout->positions_by_node_identifier == NULL)
{ {
investigation_graph_layout_set_error_literal( investigation_graph_layout_set_error_literal(
error, error,
@ -136,13 +136,13 @@ gboolean investigation_graph_layout_set_position(
} }
if (!investigation_graph_layout_identifier_is_valid( if (!investigation_graph_layout_identifier_is_valid(
entity_identifier node_identifier
)) ))
{ {
investigation_graph_layout_set_error_literal( investigation_graph_layout_set_error_literal(
error, error,
INVESTIGATION_GRAPH_LAYOUT_ERROR_INVALID_IDENTIFIER, INVESTIGATION_GRAPH_LAYOUT_ERROR_INVALID_IDENTIFIER,
"L'identifiant de l'entité n'est pas un UUID valide." "L'identifiant du nœud n'est pas un UUID valide."
); );
return FALSE; return FALSE;
@ -160,9 +160,9 @@ gboolean investigation_graph_layout_set_position(
return FALSE; return FALSE;
} }
entity_identifier_copy = node_identifier_copy =
g_strdup( g_strdup(
entity_identifier node_identifier
); );
position = position =
@ -171,7 +171,7 @@ gboolean investigation_graph_layout_set_position(
1 1
); );
if (entity_identifier_copy == NULL || if (node_identifier_copy == NULL ||
position == NULL) position == NULL)
{ {
g_free( g_free(
@ -179,7 +179,7 @@ gboolean investigation_graph_layout_set_position(
); );
g_free( g_free(
entity_identifier_copy node_identifier_copy
); );
investigation_graph_layout_set_error_literal( investigation_graph_layout_set_error_literal(
@ -198,8 +198,8 @@ gboolean investigation_graph_layout_set_position(
y; y;
g_hash_table_replace( g_hash_table_replace(
layout->positions_by_entity_identifier, layout->positions_by_node_identifier,
entity_identifier_copy, node_identifier_copy,
position position
); );
@ -208,7 +208,7 @@ gboolean investigation_graph_layout_set_position(
gboolean investigation_graph_layout_get_position( gboolean investigation_graph_layout_get_position(
const InvestigationGraphLayout *layout, const InvestigationGraphLayout *layout,
const char *entity_identifier, const char *node_identifier,
double *x, double *x,
double *y double *y
) )
@ -217,9 +217,9 @@ gboolean investigation_graph_layout_get_position(
NULL; NULL;
if (layout == NULL || if (layout == NULL ||
layout->positions_by_entity_identifier == NULL || layout->positions_by_node_identifier == NULL ||
!investigation_graph_layout_identifier_is_valid( !investigation_graph_layout_identifier_is_valid(
entity_identifier node_identifier
)) ))
{ {
return FALSE; return FALSE;
@ -227,8 +227,8 @@ gboolean investigation_graph_layout_get_position(
position = position =
g_hash_table_lookup( g_hash_table_lookup(
layout->positions_by_entity_identifier, layout->positions_by_node_identifier,
entity_identifier node_identifier
); );
if (position == NULL) if (position == NULL)
@ -253,21 +253,21 @@ gboolean investigation_graph_layout_get_position(
gboolean investigation_graph_layout_remove_position( gboolean investigation_graph_layout_remove_position(
InvestigationGraphLayout *layout, InvestigationGraphLayout *layout,
const char *entity_identifier const char *node_identifier
) )
{ {
if (layout == NULL || if (layout == NULL ||
layout->positions_by_entity_identifier == NULL || layout->positions_by_node_identifier == NULL ||
!investigation_graph_layout_identifier_is_valid( !investigation_graph_layout_identifier_is_valid(
entity_identifier node_identifier
)) ))
{ {
return FALSE; return FALSE;
} }
return g_hash_table_remove( return g_hash_table_remove(
layout->positions_by_entity_identifier, layout->positions_by_node_identifier,
entity_identifier node_identifier
); );
} }
@ -276,13 +276,13 @@ void investigation_graph_layout_clear(
) )
{ {
if (layout == NULL || if (layout == NULL ||
layout->positions_by_entity_identifier == NULL) layout->positions_by_node_identifier == NULL)
{ {
return; return;
} }
g_hash_table_remove_all( g_hash_table_remove_all(
layout->positions_by_entity_identifier layout->positions_by_node_identifier
); );
} }
@ -291,13 +291,13 @@ guint investigation_graph_layout_get_count(
) )
{ {
if (layout == NULL || if (layout == NULL ||
layout->positions_by_entity_identifier == NULL) layout->positions_by_node_identifier == NULL)
{ {
return 0U; return 0U;
} }
return g_hash_table_size( return g_hash_table_size(
layout->positions_by_entity_identifier layout->positions_by_node_identifier
); );
} }
@ -311,7 +311,7 @@ void investigation_graph_layout_free(
} }
g_clear_pointer( g_clear_pointer(
&layout->positions_by_entity_identifier, &layout->positions_by_node_identifier,
g_hash_table_unref g_hash_table_unref
); );

View file

@ -1170,10 +1170,10 @@ gboolean create_relation_dialog_present(
) )
); );
g_object_unref( /*
target_labels * Le constructeur prend possession de la référence du modèle.
); * Le dialogue ne doit donc pas appeler g_object_unref() ici.
*/
target_labels = target_labels =
NULL; NULL;

File diff suppressed because it is too large Load diff

View file

@ -156,7 +156,7 @@ static void test_graph_node_position_dao_fixture_clear(
*/ */
static void test_graph_node_position_dao_insert_entity( static void test_graph_node_position_dao_insert_entity(
Database *database, Database *database,
const char *entity_identifier, const char *node_identifier,
const char *entity_value const char *entity_value
) )
{ {
@ -168,7 +168,7 @@ static void test_graph_node_position_dao_insert_entity(
); );
g_assert_nonnull( g_assert_nonnull(
entity_identifier node_identifier
); );
g_assert_nonnull( g_assert_nonnull(
@ -208,7 +208,7 @@ static void test_graph_node_position_dao_insert_entity(
database_statement_bind_text( database_statement_bind_text(
statement, statement,
1, 1,
entity_identifier node_identifier
) )
); );
@ -254,7 +254,7 @@ static int64_t test_graph_node_position_dao_count_rows(
database_statement_prepare( database_statement_prepare(
database, database,
"SELECT COUNT(*) " "SELECT COUNT(*) "
"FROM graph_node_positions;" "FROM graph_layout_positions;"
); );
g_assert_nonnull( g_assert_nonnull(
@ -454,19 +454,16 @@ static void test_graph_node_position_dao_upsert_insert(void)
GError *error = GError *error =
NULL; NULL;
const char *entity_identifier = const char *node_identifier =
"10000000-0000-4000-8000-000000000001"; "10000000-0000-4000-8000-000000000001";
test_graph_node_position_dao_insert_entity( /* Un nœud générique peut être une relation et n'a pas à exister dans
fixture.database, * la table entites. */
entity_identifier,
"entite-position-1"
);
g_assert_true( g_assert_true(
graph_node_position_dao_upsert( graph_node_position_dao_upsert(
fixture.position_dao, fixture.position_dao,
entity_identifier, node_identifier,
120.5, 120.5,
-42.25, -42.25,
&error &error
@ -516,11 +513,11 @@ static void test_graph_node_position_dao_upsert_insert(void)
); );
g_assert_cmpstr( g_assert_cmpstr(
graph_node_position_get_entity_identifier( graph_node_position_get_node_identifier(
position position
), ),
==, ==,
entity_identifier node_identifier
); );
g_assert_cmpfloat( g_assert_cmpfloat(
@ -578,19 +575,19 @@ static void test_graph_node_position_dao_upsert_update(void)
GError *error = GError *error =
NULL; NULL;
const char *entity_identifier = const char *node_identifier =
"20000000-0000-4000-8000-000000000002"; "20000000-0000-4000-8000-000000000002";
test_graph_node_position_dao_insert_entity( test_graph_node_position_dao_insert_entity(
fixture.database, fixture.database,
entity_identifier, node_identifier,
"entite-position-2" "entite-position-2"
); );
g_assert_true( g_assert_true(
graph_node_position_dao_upsert( graph_node_position_dao_upsert(
fixture.position_dao, fixture.position_dao,
entity_identifier, node_identifier,
10.0, 10.0,
20.0, 20.0,
&error &error
@ -604,7 +601,7 @@ static void test_graph_node_position_dao_upsert_update(void)
g_assert_true( g_assert_true(
graph_node_position_dao_upsert( graph_node_position_dao_upsert(
fixture.position_dao, fixture.position_dao,
entity_identifier, node_identifier,
-300.75, -300.75,
450.5, 450.5,
&error &error
@ -855,7 +852,7 @@ static void test_graph_node_position_dao_list_order(void)
); );
g_assert_cmpstr( g_assert_cmpstr(
graph_node_position_get_entity_identifier( graph_node_position_get_node_identifier(
first_position first_position
), ),
==, ==,
@ -863,7 +860,7 @@ static void test_graph_node_position_dao_list_order(void)
); );
g_assert_cmpstr( g_assert_cmpstr(
graph_node_position_get_entity_identifier( graph_node_position_get_node_identifier(
second_position second_position
), ),
==, ==,
@ -887,19 +884,19 @@ static void test_graph_node_position_dao_delete_existing(void)
GError *error = GError *error =
NULL; NULL;
const char *entity_identifier = const char *node_identifier =
"60000000-0000-4000-8000-000000000006"; "60000000-0000-4000-8000-000000000006";
test_graph_node_position_dao_insert_entity( test_graph_node_position_dao_insert_entity(
fixture.database, fixture.database,
entity_identifier, node_identifier,
"entite-position-6" "entite-position-6"
); );
g_assert_true( g_assert_true(
graph_node_position_dao_upsert( graph_node_position_dao_upsert(
fixture.position_dao, fixture.position_dao,
entity_identifier, node_identifier,
60.0, 60.0,
70.0, 70.0,
&error &error
@ -913,7 +910,7 @@ static void test_graph_node_position_dao_delete_existing(void)
g_assert_true( g_assert_true(
graph_node_position_dao_delete( graph_node_position_dao_delete(
fixture.position_dao, fixture.position_dao,
entity_identifier, node_identifier,
&error &error
) )
); );