From 11cbc061aeef4c1e3e9de1f3a7c27fa0e2a9537b Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Sat, 1 Aug 2026 09:33:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(person):=20ajouter=20les=20relations=20fac?= =?UTF-8?q?tuelles=20contr=C3=B4l=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 19 +- diff_stat.txt | 20 ++ include/core/person_creation_coordinator.h | 2 + include/core/person_creation_task.h | 3 +- include/core/person_details_provider.h | 10 + include/dao/identity_traceability_dao.h | 4 + include/models/identity_traceability.h | 9 + include/models/person_evidence_selection.h | 2 + include/views/create_person_dialog.h | 2 + include/views/dialog_geometry.h | 1 + include/views/identity_ocr_language_loader.h | 8 + include/views/main_window.h | 7 + .../views/person_factual_relation_editor.h | 40 +++ include/views/person_vocabulary_adapter.h | 4 + include/widgets/entity_details_panel.h | 2 + include/widgets/workspace.h | 4 +- name_status.txt | 19 ++ src/core/application.c | 46 ++- src/core/person_creation_coordinator.c | 1 + src/core/person_creation_task.c | 45 ++- src/core/person_details_provider.c | 45 +++ src/dao/identity_traceability_dao.c | 52 +++- src/models/identity_traceability.c | 13 + src/models/person_evidence_selection.c | 19 ++ src/views/create_person_dialog.c | 126 +++++---- src/views/dialog_geometry.c | 35 ++- src/views/evidence_metadata_dialog.c | 58 ++++ src/views/identity_ocr_language_loader.c | 84 ++++++ src/views/main_window.c | 10 +- src/views/person_factual_relation_editor.c | 267 ++++++++++++++++++ src/views/person_vocabulary_adapter.c | 53 ++++ src/widgets/entity_details_panel.c | 62 ++++ src/widgets/workspace.c | 11 +- tests/test_create_person_dialog_gtk.c | 3 + tests/test_create_person_dialog_ocr_gtk.c | 2 + tests/test_evidence_identity_import_gtk.c | 11 +- tests/test_identity_traceability.c | 20 ++ .../test_person_factual_relation_editor_gtk.c | 62 ++++ wc_out.txt | 10 + 39 files changed, 1070 insertions(+), 121 deletions(-) create mode 100644 diff_stat.txt create mode 100644 include/core/person_details_provider.h create mode 100644 include/views/identity_ocr_language_loader.h create mode 100644 include/views/person_factual_relation_editor.h create mode 100644 name_status.txt create mode 100644 src/core/person_details_provider.c create mode 100644 src/views/identity_ocr_language_loader.c create mode 100644 src/views/person_factual_relation_editor.c create mode 100644 tests/test_person_factual_relation_editor_gtk.c create mode 100644 wc_out.txt diff --git a/Makefile b/Makefile index 249ade5..2c57cac 100644 --- a/Makefile +++ b/Makefile @@ -196,6 +196,7 @@ TEST_EVIDENCE_METADATA_DIALOG_GTK := tests/test_evidence_metadata_dialog_gtk TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk TEST_WORKSPACE_IDENTITY_OCR_GTK := tests/test_workspace_identity_ocr_gtk TEST_DIALOG_GEOMETRY_GTK := tests/test_dialog_geometry_gtk +TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK := tests/test_person_factual_relation_editor_gtk FAKE_DOCUMENT_TOOL := tests/fake_document_tool DOCUMENT_ANALYSIS_TEST_SOURCES := \ @@ -214,6 +215,16 @@ $(TEST_DIALOG_GEOMETRY_GTK): tests/test_dialog_geometry_gtk.c \ src/views/dialog_geometry.c $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) +$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK): \ + tests/test_person_factual_relation_editor_gtk.c \ + src/views/person_factual_relation_editor.c \ + src/views/person_vocabulary_adapter.c src/models/identity_ocr.c \ + src/models/identity_traceability.c src/models/person_role_assignment.c \ + src/dao/identity_traceability_dao.c src/database/database.c \ + src/database/schema.c src/database/statement.c src/database/transaction.c \ + src/database/error.c src/core/relation_type_normalizer.c + $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) + all: $(TARGET) $(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \ @@ -234,6 +245,7 @@ $(TEST_IDENTITY_OCR_PREPROCESSOR): tests/test_identity_ocr_preprocessor.c \ $(TEST_IDENTITY_TRACEABILITY): tests/test_identity_traceability.c \ src/models/identity_traceability.c src/dao/identity_traceability_dao.c \ src/views/person_vocabulary_adapter.c \ + src/views/person_factual_relation_editor.c \ src/models/person_role_assignment.c \ src/models/identity_ocr.c src/dao/identity_ocr_dao.c \ src/core/relation_type_normalizer.c \ @@ -933,6 +945,7 @@ $(TEST_PERSON_DIALOG_LIFECYCLE): tests/test_person_dialog_lifecycle.c \ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \ src/views/create_person_dialog.c src/views/dialog_geometry.c \ src/views/person_vocabulary_adapter.c \ + src/views/person_factual_relation_editor.c \ src/views/identity_ocr_option_adapter.c \ src/core/person_dialog_lifecycle.c \ src/core/person_confirmation_summary.c \ @@ -963,6 +976,7 @@ $(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \ tests/test_create_person_dialog_ocr_gtk.c \ src/views/create_person_dialog.c src/views/dialog_geometry.c \ src/views/person_vocabulary_adapter.c \ + src/views/person_factual_relation_editor.c \ src/views/identity_ocr_option_adapter.c \ src/core/person_dialog_lifecycle.c \ src/core/person_confirmation_summary.c \ @@ -1184,7 +1198,8 @@ test: \ $(TEST_EVIDENCE_METADATA_DIALOG_GTK) \ $(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \ $(TEST_WORKSPACE_IDENTITY_OCR_GTK) \ - $(TEST_DIALOG_GEOMETRY_GTK) + $(TEST_DIALOG_GEOMETRY_GTK) \ + $(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -1280,6 +1295,7 @@ test: \ @$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) @$(TEST_WORKSPACE_IDENTITY_OCR_GTK) @$(TEST_DIALOG_GEOMETRY_GTK) + @$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) @echo "Tous les tests sont valides." %.o: %.c @@ -1379,6 +1395,7 @@ clean: $(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \ $(TEST_WORKSPACE_IDENTITY_OCR_GTK) \ $(TEST_DIALOG_GEOMETRY_GTK) \ + $(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \ $(FAKE_DOCUMENT_TOOL) diff --git a/diff_stat.txt b/diff_stat.txt new file mode 100644 index 0000000..97a5e6a --- /dev/null +++ b/diff_stat.txt @@ -0,0 +1,20 @@ + Makefile | 2 + + include/core/person_creation_task.h | 3 +- + include/dao/identity_traceability_dao.h | 2 + + include/views/create_person_dialog.h | 2 + + include/views/main_window.h | 7 ++++ + include/views/person_vocabulary_adapter.h | 3 ++ + include/widgets/entity_details_panel.h | 2 + + include/widgets/workspace.h | 4 +- + src/core/application.c | 20 ++++++++-- + src/core/person_creation_task.c | 45 +++++++++++++++++++--- + src/dao/identity_traceability_dao.c | 26 +++++++++++++ + src/views/create_person_dialog.c | 63 +++++++++++++++++++++++++++---- + src/views/evidence_metadata_dialog.c | 52 +++++++++++++++++++++++++ + src/views/main_window.c | 8 +++- + src/views/person_vocabulary_adapter.c | 39 +++++++++++++++++++ + src/widgets/entity_details_panel.c | 56 +++++++++++++++++++++++++++ + src/widgets/workspace.c | 7 ++++ + tests/test_create_person_dialog_gtk.c | 3 ++ + tests/test_create_person_dialog_ocr_gtk.c | 2 + + 19 files changed, 327 insertions(+), 19 deletions(-) diff --git a/include/core/person_creation_coordinator.h b/include/core/person_creation_coordinator.h index e5f8e52..5994005 100644 --- a/include/core/person_creation_coordinator.h +++ b/include/core/person_creation_coordinator.h @@ -45,6 +45,8 @@ typedef struct { const char *relation_type; const char *factual_note; } PersonCreationFactualRelationInput; + + typedef gboolean (*PersonCreationSessionCheck)(gpointer user_data); typedef struct { PersonCreationFailurePoint failure_point; diff --git a/include/core/person_creation_task.h b/include/core/person_creation_task.h index c93cca4..665c0e4 100644 --- a/include/core/person_creation_task.h +++ b/include/core/person_creation_task.h @@ -10,7 +10,8 @@ typedef struct PersonCreationTaskRequest PersonCreationTaskRequest; PersonCreationTaskRequest *person_creation_task_request_new( const char *database_path, const char *investigation_root_path, const PersonEntityInput *person, - const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs); + const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs, + const GPtrArray *factual_relations); void person_creation_task_request_free(PersonCreationTaskRequest *request); BackgroundTask *person_creation_task_start(TaskManager *manager, const PersonCreationTaskRequest *request, diff --git a/include/core/person_details_provider.h b/include/core/person_details_provider.h new file mode 100644 index 0000000..8c239f4 --- /dev/null +++ b/include/core/person_details_provider.h @@ -0,0 +1,10 @@ +#ifndef PERSON_DETAILS_PROVIDER_H +#define PERSON_DETAILS_PROVIDER_H + +#include +#include "database/database.h" + +GPtrArray *person_details_provider_get_evidences(Database *database, const char *person_identifier, GError **error); +GPtrArray *person_details_provider_get_factual_relations(Database *database, const char *person_identifier, GError **error); + +#endif diff --git a/include/dao/identity_traceability_dao.h b/include/dao/identity_traceability_dao.h index 5fcbef5..12b1a66 100644 --- a/include/dao/identity_traceability_dao.h +++ b/include/dao/identity_traceability_dao.h @@ -21,6 +21,10 @@ gboolean identity_traceability_dao_insert_factual_relation( GError **error); GPtrArray *identity_traceability_dao_list_factual_relations( IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error); +GPtrArray *identity_traceability_dao_list_factual_relations_by_evidence( + IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error); +GPtrArray *identity_traceability_dao_list_factual_relations_by_person( + IdentityTraceabilityDao *dao,const char *person_identifier,GError **error); GPtrArray *identity_traceability_dao_list_roles( IdentityTraceabilityDao *dao,gboolean include_inactive,GError **error); GPtrArray *identity_traceability_dao_list_identification_statuses( diff --git a/include/models/identity_traceability.h b/include/models/identity_traceability.h index 95853f7..de7422c 100644 --- a/include/models/identity_traceability.h +++ b/include/models/identity_traceability.h @@ -51,6 +51,15 @@ PersonEvidenceFactualRelation *person_evidence_factual_relation_copy( const PersonEvidenceFactualRelation *relation); void person_evidence_factual_relation_free( PersonEvidenceFactualRelation *relation); +const char *person_evidence_factual_relation_get_identifier(const PersonEvidenceFactualRelation *relation); +const char *person_evidence_factual_relation_get_person_identifier(const PersonEvidenceFactualRelation *relation); +const char *person_evidence_factual_relation_get_evidence_identifier(const PersonEvidenceFactualRelation *relation); +const char *person_evidence_factual_relation_get_ocr_run_identifier(const PersonEvidenceFactualRelation *relation); +const char *person_evidence_factual_relation_get_relation_type(const PersonEvidenceFactualRelation *relation); +const char *person_evidence_factual_relation_get_factual_note(const PersonEvidenceFactualRelation *relation); +const char *person_evidence_factual_relation_get_observed_at(const PersonEvidenceFactualRelation *relation); +const char *person_evidence_factual_relation_get_origin(const PersonEvidenceFactualRelation *relation); +gboolean person_evidence_factual_relation_get_active(const PersonEvidenceFactualRelation *relation); void person_role_vocabulary_entry_free(PersonRoleVocabularyEntry *entry); void identification_status_vocabulary_entry_free( IdentificationStatusVocabularyEntry *entry); diff --git a/include/models/person_evidence_selection.h b/include/models/person_evidence_selection.h index f6b00e8..928bf20 100644 --- a/include/models/person_evidence_selection.h +++ b/include/models/person_evidence_selection.h @@ -72,6 +72,8 @@ const char *person_evidence_selection_item_get_original_name( const PersonEvidenceSelectionItem *item); const char *person_evidence_selection_item_get_mime_type( const PersonEvidenceSelectionItem *item); +const char *person_evidence_selection_item_get_effective_ocr_mime( + const PersonEvidenceSelectionItem *item); const char *person_evidence_selection_item_get_type_identifier( const PersonEvidenceSelectionItem *item); const char *person_evidence_selection_item_get_sha256( diff --git a/include/views/create_person_dialog.h b/include/views/create_person_dialog.h index 8c2d9a4..0a83db5 100644 --- a/include/views/create_person_dialog.h +++ b/include/views/create_person_dialog.h @@ -44,6 +44,8 @@ EvidenceStaging *create_person_dialog_result_steal_staging( CreatePersonDialogResult *result); const GPtrArray *create_person_dialog_result_get_ocr_runs( const CreatePersonDialogResult *result); +const GPtrArray *create_person_dialog_result_get_factual_relations( + const CreatePersonDialogResult *result); gboolean create_person_dialog_test_overlay_has_region(GtkWindow *window); guint64 create_person_dialog_test_ocr_generation(GtkWindow *window); G_END_DECLS diff --git a/include/views/dialog_geometry.h b/include/views/dialog_geometry.h index 0d6138d..f3f44c7 100644 --- a/include/views/dialog_geometry.h +++ b/include/views/dialog_geometry.h @@ -26,5 +26,6 @@ void labfy_dialog_present(GtkWindow *window); void labfy_paned_apply_initial_ratio( GtkPaned *paned, double ratio, int minimum_start, int minimum_end); +gboolean labfy_paned_initial_ratio_applied(GtkPaned *paned); #endif diff --git a/include/views/identity_ocr_language_loader.h b/include/views/identity_ocr_language_loader.h new file mode 100644 index 0000000..f9f6466 --- /dev/null +++ b/include/views/identity_ocr_language_loader.h @@ -0,0 +1,8 @@ +#ifndef IDENTITY_OCR_LANGUAGE_LOADER_H +#define IDENTITY_OCR_LANGUAGE_LOADER_H + +#include + +void identity_ocr_language_loader_start(GtkWindow *window, const char *tesseract_path, GtkDropDown *language_dropdown); + +#endif diff --git a/include/views/main_window.h b/include/views/main_window.h index 5be1f4b..c25e121 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -322,6 +322,13 @@ void main_window_set_relation_evidences( const GPtrArray *evidence_records ); +/** @brief Transmet les preuves associées à l'entité sélectionnée. */ +void main_window_set_person_evidences(MainWindow *main_window, + const GPtrArray *records); +void main_window_set_person_factual_relations(MainWindow *main_window, + const GPtrArray *relations, const GPtrArray *evidence_records); + + /** @brief Définit le callback de catégorisation d'une personne. */ void main_window_set_person_role_callback(MainWindow *main_window, MainWindowPersonRoleCallback callback, gpointer user_data); diff --git a/include/views/person_factual_relation_editor.h b/include/views/person_factual_relation_editor.h new file mode 100644 index 0000000..63f6de6 --- /dev/null +++ b/include/views/person_factual_relation_editor.h @@ -0,0 +1,40 @@ +#ifndef LABFY_PERSON_FACTUAL_RELATION_EDITOR_H +#define LABFY_PERSON_FACTUAL_RELATION_EDITOR_H + +#include +#include "core/person_creation_coordinator.h" +#include "models/identity_ocr.h" + +G_BEGIN_DECLS + +typedef struct PersonFactualRelationEditor PersonFactualRelationEditor; + +PersonFactualRelationEditor *person_factual_relation_editor_new(void); + +GtkWidget *person_factual_relation_editor_get_widget( + PersonFactualRelationEditor *editor); + +void person_factual_relation_editor_set_available_evidence( + PersonFactualRelationEditor *editor, + GtkStringList *evidence_labels, + GPtrArray *evidence_identifiers); + +void person_factual_relation_editor_set_available_ocr_runs( + PersonFactualRelationEditor *editor, + GPtrArray *ocr_runs); + +gboolean person_factual_relation_editor_collect_relations( + PersonFactualRelationEditor *editor, GPtrArray **relations, + GError **error); +guint person_factual_relation_editor_get_count( + const PersonFactualRelationEditor *editor); +gboolean person_factual_relation_editor_validate( + PersonFactualRelationEditor *editor, GError **error); +void person_factual_relation_editor_append_summary( + PersonFactualRelationEditor *editor, GString *summary); + +void person_factual_relation_editor_free(PersonFactualRelationEditor *editor); + +G_END_DECLS + +#endif diff --git a/include/views/person_vocabulary_adapter.h b/include/views/person_vocabulary_adapter.h index e01aa24..2d35640 100644 --- a/include/views/person_vocabulary_adapter.h +++ b/include/views/person_vocabulary_adapter.h @@ -16,6 +16,10 @@ GtkStringList *person_vocabulary_adapter_create_role_labels( const PersonVocabularyAdapter *adapter); GtkStringList *person_vocabulary_adapter_create_status_labels( const PersonVocabularyAdapter *adapter); +GtkStringList *person_vocabulary_adapter_create_relation_labels(void); +const char *person_vocabulary_adapter_relation_code(guint index); +const char *person_vocabulary_adapter_relation_label(const char *code); +const char *person_vocabulary_adapter_relation_description(const char *code); const char *person_vocabulary_adapter_status_code( const PersonVocabularyAdapter *adapter, guint index); const char *person_vocabulary_adapter_status_label( diff --git a/include/widgets/entity_details_panel.h b/include/widgets/entity_details_panel.h index 31001f4..14d02bf 100644 --- a/include/widgets/entity_details_panel.h +++ b/include/widgets/entity_details_panel.h @@ -195,6 +195,8 @@ void entity_details_panel_set_osint_callback(EntityDetailsPanel *details_panel, */ void entity_details_panel_set_person_evidences( EntityDetailsPanel *details_panel, const GPtrArray *evidence_records); +void entity_details_panel_set_person_factual_relations( + EntityDetailsPanel *details_panel, const GPtrArray *relations, const GPtrArray *evidence_records); /** * @brief Indique si le volet est actuellement ouvert. diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 83e875a..6e8bdea 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -472,7 +472,9 @@ void workspace_set_entity_selected_callback(Workspace *workspace, WorkspaceEntitySelectedCallback callback, gpointer user_data); /** @brief Affiche les preuves de la personne sélectionnée. */ void workspace_set_person_evidences(Workspace *workspace, - const GPtrArray *evidence_records); + const GPtrArray *records); +void workspace_set_person_factual_relations(Workspace *workspace, + const GPtrArray *relations, const GPtrArray *evidence_records); /** * @brief Affiche le graphe chargé avec sa disposition persistée. diff --git a/name_status.txt b/name_status.txt new file mode 100644 index 0000000..f92382d --- /dev/null +++ b/name_status.txt @@ -0,0 +1,19 @@ +M Makefile +M include/core/person_creation_task.h +M include/dao/identity_traceability_dao.h +M include/views/create_person_dialog.h +M include/views/main_window.h +M include/views/person_vocabulary_adapter.h +M include/widgets/entity_details_panel.h +M include/widgets/workspace.h +M src/core/application.c +M src/core/person_creation_task.c +M src/dao/identity_traceability_dao.c +M src/views/create_person_dialog.c +M src/views/evidence_metadata_dialog.c +M src/views/main_window.c +M src/views/person_vocabulary_adapter.c +M src/widgets/entity_details_panel.c +M src/widgets/workspace.c +M tests/test_create_person_dialog_gtk.c +M tests/test_create_person_dialog_ocr_gtk.c diff --git a/src/core/application.c b/src/core/application.c index 0d53a1c..53e5d48 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -47,6 +47,7 @@ #include "dao/evidence_dao.h" #include "dao/identity_ocr_dao.h" #include "dao/evidence_entity_dao.h" +#include "core/person_details_provider.h" #include "dao/entity_dao.h" #include "dao/relation_dao.h" #include "dao/relation_evidence_dao.h" @@ -6044,7 +6045,8 @@ static void application_on_person_completed( goto changed_session; request = person_creation_task_request_new(active_database, active_root, input, create_person_dialog_result_get_evidence_selection(result), - create_person_dialog_result_get_ocr_runs(result)); + create_person_dialog_result_get_ocr_runs(result), + create_person_dialog_result_get_factual_relations(result)); task_context = g_new0(ApplicationPersonCreationTaskContext, 1); task_context->application = application; task_context->generation = application->session_generation; @@ -7058,36 +7060,26 @@ static void application_on_person_name_changed(const char *entity_identifier, static void application_on_entity_selected(const char *entity_identifier, gpointer user_data) { - Application *application = user_data; Database *database = NULL; - EvidenceEntityDao *link_dao = NULL; EvidenceDao *evidence_dao = NULL; - GPtrArray *identifiers = NULL; GPtrArray *records = NULL; - GError *error = NULL; + Application *application = user_data; if (application == NULL || application->session == NULL || entity_identifier == NULL) return; - database = investigation_session_get_database(application->session); - link_dao = evidence_entity_dao_new(database, &error); - if (link_dao == NULL) goto cleanup; - evidence_dao = evidence_dao_new(database, &error); - if (evidence_dao == NULL) goto cleanup; - identifiers = evidence_entity_dao_list_evidence_identifiers(link_dao, - entity_identifier, &error); - if (identifiers == NULL) goto cleanup; - records = g_ptr_array_new_with_free_func((GDestroyNotify) - evidence_record_free); - for (guint index = 0; index < identifiers->len; index++) - { - EvidenceRecord *record = evidence_dao_find_by_identifier(evidence_dao, - g_ptr_array_index(identifiers, index), &error); - if (record == NULL) goto cleanup; - g_ptr_array_add(records, record); + Database *database = investigation_session_get_database(application->session); + GError *error = NULL; + GPtrArray *records = person_details_provider_get_evidences(database, entity_identifier, &error); + GPtrArray *relations = NULL; + if (records != NULL) { + relations = person_details_provider_get_factual_relations(database, entity_identifier, NULL); } + main_window_set_person_evidences(application->main_window, records); -cleanup: - if (error != NULL) - main_window_set_person_evidences(application->main_window, NULL); - g_clear_error(&error); g_clear_pointer(&records, g_ptr_array_unref); - g_clear_pointer(&identifiers, g_ptr_array_unref); - evidence_dao_free(evidence_dao); evidence_entity_dao_free(link_dao); + main_window_set_person_factual_relations(application->main_window, relations, records); + + if (error != NULL) { + application_present_error(application, "Erreur de chargement", error->message); + g_clear_error(&error); + } + if (records != NULL) g_ptr_array_unref(records); + if (relations != NULL) g_ptr_array_unref(relations); } /** @brief Contexte possédé par la gestion des preuves d'une personne. */ diff --git a/src/core/person_creation_coordinator.c b/src/core/person_creation_coordinator.c index 567f79d..87b5f52 100644 --- a/src/core/person_creation_coordinator.c +++ b/src/core/person_creation_coordinator.c @@ -11,6 +11,7 @@ #include #include + static GQuark coordinator_error(void) { return g_quark_from_static_string("person-creation-coordinator-error"); diff --git a/src/core/person_creation_task.c b/src/core/person_creation_task.c index 0081c05..1c7ec26 100644 --- a/src/core/person_creation_task.c +++ b/src/core/person_creation_task.c @@ -12,11 +12,13 @@ struct PersonCreationTaskRequest { GPtrArray *roles; PersonEvidenceSelection *selection; GPtrArray *ocr_runs; + GPtrArray *factual_relations; }; PersonCreationTaskRequest *person_creation_task_request_new( const char *database_path, const char *root, const PersonEntityInput *person, - const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs) + const PersonEvidenceSelection *selection, const GPtrArray *ocr_runs, + const GPtrArray *factual_relations) { PersonCreationTaskRequest *request; if (database_path == NULL || root == NULL || person == NULL) return NULL; @@ -49,6 +51,21 @@ PersonCreationTaskRequest *person_creation_task_request_new( for (guint i = 0; ocr_runs != NULL && i < ocr_runs->len; i++) g_ptr_array_add(request->ocr_runs, identity_ocr_run_copy(g_ptr_array_index((GPtrArray *) ocr_runs,i))); + + request->factual_relations = NULL; + if (factual_relations != NULL) { + request->factual_relations = g_ptr_array_new_with_free_func(g_free); + for (guint i = 0; i < factual_relations->len; i++) { + PersonCreationFactualRelationInput *orig = g_ptr_array_index((GPtrArray *) factual_relations, i); + PersonCreationFactualRelationInput *copy_rel = g_new0(PersonCreationFactualRelationInput, 1); + copy_rel->evidence_selection_identifier = g_strdup(orig->evidence_selection_identifier); + copy_rel->ocr_run_identifier = g_strdup(orig->ocr_run_identifier); + copy_rel->relation_type = g_strdup(orig->relation_type); + copy_rel->factual_note = g_strdup(orig->factual_note); + g_ptr_array_add(request->factual_relations, copy_rel); + } + } + return request; } void person_creation_task_request_free(PersonCreationTaskRequest *request) @@ -60,6 +77,16 @@ void person_creation_task_request_free(PersonCreationTaskRequest *request) g_free(request->notes); g_ptr_array_unref(request->roles); person_evidence_selection_free(request->selection); g_ptr_array_unref(request->ocr_runs); + if (request->factual_relations) { + for (guint i = 0; i < request->factual_relations->len; i++) { + PersonCreationFactualRelationInput *rel = g_ptr_array_index(request->factual_relations, i); + g_free((char*)rel->evidence_selection_identifier); + g_free((char*)rel->ocr_run_identifier); + g_free((char*)rel->relation_type); + g_free((char*)rel->factual_note); + } + g_ptr_array_unref(request->factual_relations); + } g_free(request); } static gboolean worker(BackgroundTask *task, GCancellable *cancellable, @@ -75,9 +102,17 @@ static gboolean worker(BackgroundTask *task, GCancellable *cancellable, "Impossible d’ouvrir la base de l’enquête."); return FALSE; } - *result = person_creation_coordinator_execute(database, request->root, - &request->person, request->selection, request->ocr_runs, - cancellable, error); + if (request->factual_relations != NULL && request->factual_relations->len > 0) { + PersonCreationCoordinatorOptions options = {0}; + options.factual_relations = request->factual_relations; + *result = person_creation_coordinator_execute_with_options(database, request->root, + &request->person, request->selection, request->ocr_runs, &options, + cancellable, error); + } else { + *result = person_creation_coordinator_execute(database, request->root, + &request->person, request->selection, request->ocr_runs, + cancellable, error); + } database_close(database); return *result != NULL; } @@ -92,7 +127,7 @@ BackgroundTask *person_creation_task_start(TaskManager *manager, task = background_task_new("Création de la personne et import des preuves"); copy = person_creation_task_request_new(request->database_path, request->root, &request->person, request->selection, - request->ocr_runs); + request->ocr_runs, request->factual_relations); if (task == NULL || copy == NULL || !task_manager_add(manager, task, error) || !background_task_start(task, worker, copy, diff --git a/src/core/person_details_provider.c b/src/core/person_details_provider.c new file mode 100644 index 0000000..3168a62 --- /dev/null +++ b/src/core/person_details_provider.c @@ -0,0 +1,45 @@ +#include "core/person_details_provider.h" +#include "dao/evidence_dao.h" +#include "dao/evidence_entity_dao.h" +#include "dao/identity_traceability_dao.h" +#include "models/evidence_record.h" + +GPtrArray *person_details_provider_get_evidences(Database *database, const char *person_identifier, GError **error) +{ + EvidenceEntityDao *link_dao = evidence_entity_dao_new(database, error); + if (link_dao == NULL) return NULL; + EvidenceDao *evidence_dao = evidence_dao_new(database, error); + if (evidence_dao == NULL) { + evidence_entity_dao_free(link_dao); + return NULL; + } + GPtrArray *identifiers = evidence_entity_dao_list_evidence_identifiers(link_dao, person_identifier, error); + if (identifiers == NULL) { + evidence_dao_free(evidence_dao); + evidence_entity_dao_free(link_dao); + return NULL; + } + GPtrArray *records = g_ptr_array_new_with_free_func((GDestroyNotify)evidence_record_free); + for (guint index = 0; index < identifiers->len; index++) { + EvidenceRecord *record = evidence_dao_find_by_identifier(evidence_dao, g_ptr_array_index(identifiers, index), error); + if (record == NULL) { + g_ptr_array_unref(records); + records = NULL; + break; + } + g_ptr_array_add(records, record); + } + g_ptr_array_unref(identifiers); + evidence_dao_free(evidence_dao); + evidence_entity_dao_free(link_dao); + return records; +} + +GPtrArray *person_details_provider_get_factual_relations(Database *database, const char *person_identifier, GError **error) +{ + IdentityTraceabilityDao *trace_dao = identity_traceability_dao_new(database); + if (trace_dao == NULL) return NULL; + GPtrArray *relations = identity_traceability_dao_list_factual_relations_by_person(trace_dao, person_identifier, error); + identity_traceability_dao_free(trace_dao); + return relations; +} diff --git a/src/dao/identity_traceability_dao.c b/src/dao/identity_traceability_dao.c index 73b6ea1..2b94782 100644 --- a/src/dao/identity_traceability_dao.c +++ b/src/dao/identity_traceability_dao.c @@ -78,12 +78,27 @@ gboolean identity_traceability_dao_insert_factual_relation( { PersonEvidenceFactualRelation*valid=r?person_evidence_factual_relation_copy(r):NULL; if(!d||!valid){fail(e,"Relation factuelle invalide.");return FALSE;} + const char*run=person_evidence_factual_relation_get_ocr_run_identifier(valid); + if(run){ + DatabaseStatement*check=database_statement_prepare(d->database, + "SELECT 1 FROM identity_ocr_runs WHERE id=? AND evidence_id=?;"); + gboolean compatible=check&&bind(check,1,run)&&bind(check,2, + person_evidence_factual_relation_get_evidence_identifier(valid))&& + database_statement_step(check)==DATABASE_STATEMENT_STEP_ROW; + database_statement_finalize(check); + if(!compatible){person_evidence_factual_relation_free(valid); + fail(e,"L’exécution OCR ne correspond pas à la preuve choisie.");return FALSE;} + } DatabaseStatement*s=database_statement_prepare(d->database, "INSERT INTO person_evidence_factual_relations VALUES(?,?,?,?,?,?,?,?,?);"); - gboolean ok=s&&bind(s,1,r->identifier)&&bind(s,2,r->person_identifier)&& - bind(s,3,r->evidence_identifier)&&bind(s,4,r->ocr_run_identifier)&& - bind(s,5,r->relation_type)&&bind(s,6,r->factual_note)&&bind(s,7,r->observed_at)&& - bind(s,8,"human")&&database_statement_bind_int64(s,9,r->active?1:0)&& + gboolean ok=s&&bind(s,1,person_evidence_factual_relation_get_identifier(valid))&& + bind(s,2,person_evidence_factual_relation_get_person_identifier(valid))&& + bind(s,3,person_evidence_factual_relation_get_evidence_identifier(valid))&& + bind(s,4,run)&&bind(s,5,person_evidence_factual_relation_get_relation_type(valid))&& + bind(s,6,person_evidence_factual_relation_get_factual_note(valid))&& + bind(s,7,person_evidence_factual_relation_get_observed_at(valid))&& + bind(s,8,"human")&&database_statement_bind_int64(s,9, + person_evidence_factual_relation_get_active(valid)?1:0)&& database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE; database_statement_finalize(s);person_evidence_factual_relation_free(valid); if(!ok)fail(e,"Impossible de conserver la relation factuelle."); @@ -115,6 +130,35 @@ GPtrArray *identity_traceability_dao_list_factual_relations( bad:database_statement_finalize(s);g_ptr_array_unref(a); fail(e,"Impossible de lire les relations factuelles.");return NULL; } +GPtrArray *identity_traceability_dao_list_factual_relations_by_evidence( + IdentityTraceabilityDao*d,const char*id,GError**e) +{return identity_traceability_dao_list_factual_relations(d,id,e);} +GPtrArray *identity_traceability_dao_list_factual_relations_by_person( + IdentityTraceabilityDao*d,const char*id,GError**e) +{ + if(!d||!id){fail(e,"Lecture des relations factuelles invalide.");return NULL;} + DatabaseStatement*s=database_statement_prepare(d->database, + "SELECT id,person_id,evidence_id,ocr_run_id,relation_type,factual_note," + "observed_at,origin,active FROM person_evidence_factual_relations " + "WHERE person_id=? ORDER BY observed_at,id;"); + GPtrArray*a=g_ptr_array_new_with_free_func( + (GDestroyNotify)person_evidence_factual_relation_free); + if(!s||!bind(s,1,id))goto bad2; + for(;;){DatabaseStatementStepResult step=database_statement_step(s); + if(step==DATABASE_STATEMENT_STEP_DONE)break; + if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad2; + char*v[8]={0};gint64 active=0;gboolean ok=TRUE; + for(int i=0;i<8;i++)ok=ok&&database_statement_column_text(s,i,&v[i]); + ok=ok&&database_statement_column_int64(s,8,&active); + PersonEvidenceFactualRelation*r=ok?person_evidence_factual_relation_new( + v[0],v[1],v[2],v[3],v[4],v[5],v[6],active!=0):NULL; + for(int i=0;i<8;i++)g_free(v[i]); + if(!r)goto bad2; + g_ptr_array_add(a,r);} + database_statement_finalize(s);return a; +bad2:database_statement_finalize(s);g_ptr_array_unref(a); + fail(e,"Impossible de lire les relations factuelles.");return NULL; +} GPtrArray *identity_traceability_dao_list_roles( IdentityTraceabilityDao*d,gboolean inactive,GError**e) { diff --git a/src/models/identity_traceability.c b/src/models/identity_traceability.c index c962c77..6d2a5f1 100644 --- a/src/models/identity_traceability.c +++ b/src/models/identity_traceability.c @@ -102,6 +102,19 @@ void person_evidence_factual_relation_free(PersonEvidenceFactualRelation*r) g_free(r->evidence_identifier);g_free(r->ocr_run_identifier); g_free(r->relation_type);g_free(r->factual_note);g_free(r->observed_at); g_free(r->origin);g_free(r);} +#define FACTUAL_GETTER(name,field) \ +const char *person_evidence_factual_relation_get_##name( \ + const PersonEvidenceFactualRelation*r){return r?r->field:NULL;} +FACTUAL_GETTER(identifier,identifier) +FACTUAL_GETTER(person_identifier,person_identifier) +FACTUAL_GETTER(evidence_identifier,evidence_identifier) +FACTUAL_GETTER(ocr_run_identifier,ocr_run_identifier) +FACTUAL_GETTER(relation_type,relation_type) +FACTUAL_GETTER(factual_note,factual_note) +FACTUAL_GETTER(observed_at,observed_at) +FACTUAL_GETTER(origin,origin) +gboolean person_evidence_factual_relation_get_active( + const PersonEvidenceFactualRelation*r){return r?r->active:FALSE;} void person_role_vocabulary_entry_free(PersonRoleVocabularyEntry*e) {if(!e)return;g_free(e->code);g_free(e->label);g_free(e->description);g_free(e);} void identification_status_vocabulary_entry_free( diff --git a/src/models/person_evidence_selection.c b/src/models/person_evidence_selection.c index b2b2876..4bae89c 100644 --- a/src/models/person_evidence_selection.c +++ b/src/models/person_evidence_selection.c @@ -279,6 +279,25 @@ const char *person_evidence_selection_find_existing_by_sha256( } return NULL; } + + + +const char *person_evidence_selection_item_get_effective_ocr_mime( + const PersonEvidenceSelectionItem *item) +{ + const char *mime = person_evidence_selection_item_get_mime_type(item); + if (mime != NULL && mime[0] != '\0') return mime; + const char *name = person_evidence_selection_item_get_original_name(item); + const char *suffix = name != NULL ? strrchr(name, '.') : NULL; + if (suffix == NULL) return NULL; + if (g_ascii_strcasecmp(suffix, ".png") == 0) return "image/png"; + if (g_ascii_strcasecmp(suffix, ".jpg") == 0 || g_ascii_strcasecmp(suffix, ".jpeg") == 0) return "image/jpeg"; + if (g_ascii_strcasecmp(suffix, ".heic") == 0) return "image/heic"; + if (g_ascii_strcasecmp(suffix, ".heif") == 0) return "image/heif"; + if (g_ascii_strcasecmp(suffix, ".pdf") == 0) return "application/pdf"; + return NULL; +} + #define ITEM_GETTER(name, type, field) \ type person_evidence_selection_item_get_##name( \ const PersonEvidenceSelectionItem *item) { \ diff --git a/src/views/create_person_dialog.c b/src/views/create_person_dialog.c index b907ebf..ae3af1f 100644 --- a/src/views/create_person_dialog.c +++ b/src/views/create_person_dialog.c @@ -6,6 +6,7 @@ #include "views/dialog_geometry.h" #include "views/person_vocabulary_adapter.h" #include "views/identity_ocr_option_adapter.h" +#include "views/person_factual_relation_editor.h" #include "core/evidence_staging.h" #include "core/evidence_staging_task.h" #include "core/person_confirmation_summary.h" @@ -33,6 +34,7 @@ struct CreatePersonDialogResult PersonEvidenceSelection *evidence_selection; EvidenceStaging *staging; GPtrArray *ocr_runs; + GPtrArray *factual_relations; }; typedef struct { @@ -107,6 +109,7 @@ typedef struct gulong retained_selected_handler; gboolean updating_evidence; gboolean updating_retained; + PersonFactualRelationEditor *factual_relation_editor; } CreatePersonDialogState; typedef struct { @@ -152,8 +155,7 @@ static void identity_languages_worker(GTask *task, gpointer source, GError *error = NULL; char *raw = ocr_analysis_list_languages(job->executable, cancellable, &error); - GPtrArray *parsed = raw != NULL - ? ocr_analysis_parse_languages(raw) : NULL; + GPtrArray *parsed = raw != NULL ? ocr_analysis_parse_languages(raw) : NULL; GPtrArray *choices = parsed != NULL ? ocr_analysis_build_language_choices(parsed) : NULL; g_free(raw); @@ -163,35 +165,6 @@ static void identity_languages_worker(GTask *task, gpointer source, (GDestroyNotify) g_ptr_array_unref); else g_task_return_error(task, error); } - -static const char *create_person_dialog_effective_ocr_mime( - const PersonEvidenceSelectionItem *item) -{ - const char *mime = - person_evidence_selection_item_get_mime_type(item); - const char *name; - const char *suffix; - - if (mime != NULL && mime[0] != '\0') - return mime; - name = person_evidence_selection_item_get_original_name(item); - suffix = name != NULL ? strrchr(name, '.') : NULL; - if (suffix == NULL) - return NULL; - if (g_ascii_strcasecmp(suffix, ".png") == 0) - return "image/png"; - if (g_ascii_strcasecmp(suffix, ".jpg") == 0 || - g_ascii_strcasecmp(suffix, ".jpeg") == 0) - return "image/jpeg"; - if (g_ascii_strcasecmp(suffix, ".heic") == 0) - return "image/heic"; - if (g_ascii_strcasecmp(suffix, ".heif") == 0) - return "image/heif"; - if (g_ascii_strcasecmp(suffix, ".pdf") == 0) - return "application/pdf"; - return NULL; -} - static void identity_languages_completed(GObject *source, GAsyncResult *result, gpointer data) { @@ -231,15 +204,6 @@ static void identity_languages_completed(GObject *source, g_clear_object(&window); } -/** @brief Copie et nettoie un texte facultatif. */ -static char *create_person_dialog_copy(const char *text) -{ - char *copy = text != NULL ? g_strdup(text) : NULL; - if (copy == NULL) return NULL; - g_strstrip(copy); - if (copy[0] == '\0') { g_free(copy); return NULL; } - return copy; -} /** @brief Extrait les notes de la zone de texte. */ static char *create_person_dialog_get_notes(CreatePersonDialogState *state) { @@ -611,6 +575,10 @@ static void create_person_dialog_ocr_completed(GObject *source, g_ptr_array_index((GPtrArray *) fields, 0), state->ocr_generation); g_ptr_array_add(state->ocr_runs, run); + if (state->factual_relation_editor != NULL) { + person_factual_relation_editor_set_available_ocr_runs( + state->factual_relation_editor, state->ocr_runs); + } run = NULL; gtk_label_set_text(state->ocr_status, "OCR terminé. Chaque proposition reste À vérifier."); @@ -660,7 +628,7 @@ static void create_person_dialog_on_ocr_start(GtkButton *button, job->identifier = g_strdup( person_evidence_selection_item_get_identifier(item)); job->sha256 = g_strdup(person_evidence_selection_item_get_sha256(item)); - job->mime = g_strdup(create_person_dialog_effective_ocr_mime(item)); + job->mime = g_strdup(person_evidence_selection_item_get_effective_ocr_mime(item)); if (person_evidence_selection_item_get_origin(item) == PERSON_EVIDENCE_ORIGIN_EXISTING) { record = person_evidence_selection_item_get_record(item); @@ -743,6 +711,7 @@ static void create_person_dialog_state_free(gpointer data) g_free(state->investigation_root_path); g_free(state->tesseract_path); g_free(state->tesseract_version); + person_factual_relation_editor_free(state->factual_relation_editor); if (state->user_data_destroy != NULL) state->user_data_destroy(state->user_data); g_free(state); @@ -1298,15 +1267,15 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data) } notes = create_person_dialog_get_notes(state); result = g_new0(CreatePersonDialogResult, 1); - result->designation = create_person_dialog_copy( - gtk_editable_get_text(GTK_EDITABLE(state->designation))); - result->declared_name = create_person_dialog_copy( - gtk_editable_get_text(GTK_EDITABLE(state->name))); - result->pseudonym = create_person_dialog_copy( - gtk_editable_get_text(GTK_EDITABLE(state->pseudonym))); - result->status = g_strdup(person_vocabulary_adapter_status_code( - state->vocabularies, status)); - result->notes = create_person_dialog_copy(notes); + result->designation = g_strdup(gtk_editable_get_text(GTK_EDITABLE(state->designation))); + result->declared_name = g_strdup(gtk_editable_get_text(GTK_EDITABLE(state->name))); + result->pseudonym = g_strdup(gtk_editable_get_text(GTK_EDITABLE(state->pseudonym))); + result->status = g_strdup(person_vocabulary_adapter_status_code(state->vocabularies, status)); + result->notes = g_strdup(notes); + if (result->designation) { g_strstrip(result->designation); if (result->designation[0] == '\0') { g_free(result->designation); result->designation = NULL; } } + if (result->declared_name) { g_strstrip(result->declared_name); if (result->declared_name[0] == '\0') { g_free(result->declared_name); result->declared_name = NULL; } } + if (result->pseudonym) { g_strstrip(result->pseudonym); if (result->pseudonym[0] == '\0') { g_free(result->pseudonym); result->pseudonym = NULL; } } + if (result->notes) { g_strstrip(result->notes); if (result->notes[0] == '\0') { g_free(result->notes); result->notes = NULL; } } if (person_evidence_selection_get_count( state->person_evidence_selection) > 0) { const PersonEvidenceSelectionItem *first = @@ -1327,6 +1296,14 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data) state->vocabularies, state->role_buttons, result->evidence_identifier); result->input.role_assignments = result->role_assignments; + GError *relation_error = NULL; + if (!person_factual_relation_editor_collect_relations(state->factual_relation_editor, + &result->factual_relations, &relation_error)) { + gtk_label_set_text(state->error, relation_error->message); + gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE); + g_clear_error(&relation_error); create_person_dialog_result_free(result); + g_free(notes); return; + } result->evidence_selection = state->person_evidence_selection; state->person_evidence_selection = NULL; result->staging = state->staging; @@ -1346,10 +1323,10 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data) static void create_person_dialog_update_navigation(CreatePersonDialogState *state) { static const char *const pages[] = { - "person", "roles", "evidence", "identity-ocr", "summary"}; + "person", "roles", "evidence", "identity-ocr", "factual-relations", "summary"}; static const char *const steps[] = { "1 Personne", "2 Rôles", "3 Preuves", "4 OCR identité", - "5 Confirmation"}; + "5 Relations factuelles", "6 Confirmation"}; GString *progress = g_string_new(NULL); gtk_stack_set_visible_child_name(state->stack, pages[state->step]); for (guint i = 0; i < G_N_ELEMENTS(steps); i++) { @@ -1363,7 +1340,7 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat } gtk_label_set_markup(state->progress, progress->str); g_string_free(progress, TRUE); - if (state->step == 4) { + if (state->step == 5) { GPtrArray *role_labels = person_vocabulary_adapter_selected_role_labels( state->role_buttons); @@ -1381,6 +1358,8 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat gtk_spin_button_get_value_as_int(state->confidence), notes, role_labels, state->person_evidence_selection); GString *confirmation = g_string_new(text); + person_factual_relation_editor_append_summary(state->factual_relation_editor, + confirmation); g_string_append(confirmation, "\n\nOCR identité\nL’OCR peut contenir des erreurs. Ces " "informations sont seulement présentées sur le document ; " @@ -1436,10 +1415,11 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat g_ptr_array_unref(role_labels); } gtk_widget_set_sensitive(GTK_WIDGET(state->previous), state->step > 0); - gtk_widget_set_visible(GTK_WIDGET(state->next), state->step < 4); - gtk_widget_set_visible(GTK_WIDGET(state->create), state->step == 4); + gtk_widget_set_visible(GTK_WIDGET(state->next), state->step < 5); + gtk_widget_set_visible(GTK_WIDGET(state->create), state->step == 5); gtk_widget_set_sensitive(GTK_WIDGET(state->create), - state->step == 4 && + state->step == 5 && + gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] != '\0' && person_evidence_selection_is_confirmable( state->person_evidence_selection)); } @@ -1463,8 +1443,17 @@ static void create_person_dialog_on_next(GtkButton *button, gpointer data) create_person_dialog_capture_corrected_transcription(state); if (state->step == 3) create_person_dialog_capture_factual_notes(state); + if (state->step == 4) { + GError *error = NULL; + if (!person_factual_relation_editor_validate(state->factual_relation_editor, + &error)) { + gtk_label_set_text(state->error, error->message); + gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE); + g_clear_error(&error); return; + } + } gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE); - if (state->step < 4) state->step++; + if (state->step < 5) state->step++; create_person_dialog_update_navigation(state); } /** @brief Ajoute une ligne libellée au formulaire. */ @@ -1870,8 +1859,21 @@ gboolean create_person_dialog_present(GtkWindow *parent, state->summary_scroll, FALSE); gtk_widget_set_vexpand(GTK_WIDGET(state->summary_scroll), TRUE); gtk_scrolled_window_set_child(state->summary_scroll, summary); + + state->factual_relation_editor = person_factual_relation_editor_new(); + person_factual_relation_editor_set_available_evidence(state->factual_relation_editor, + state->evidence_labels, state->evidence_identifiers); + person_factual_relation_editor_set_available_ocr_runs(state->factual_relation_editor, + state->ocr_runs); + GtkWidget *relation_scroll = gtk_scrolled_window_new(); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(relation_scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(relation_scroll), + person_factual_relation_editor_get_widget(state->factual_relation_editor)); + gtk_stack_add_titled(state->stack, relation_scroll, "factual-relations", + "5 — Relations factuelles"); + gtk_stack_add_titled(state->stack, - GTK_WIDGET(state->summary_scroll), "summary", "5 — Confirmation"); + GTK_WIDGET(state->summary_scroll), "summary", "6 — Confirmation"); state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE); actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_name(actions, "create-person-actions"); @@ -1950,6 +1952,9 @@ void create_person_dialog_result_free(CreatePersonDialogResult *result) person_evidence_selection_free); g_clear_pointer(&result->staging, evidence_staging_free); g_clear_pointer(&result->ocr_runs, g_ptr_array_unref); + if (result->factual_relations) { + g_ptr_array_unref(result->factual_relations); + } g_free(result); } const PersonEntityInput *create_person_dialog_result_get_input( @@ -1975,6 +1980,11 @@ const GPtrArray *create_person_dialog_result_get_ocr_runs( { return result != NULL ? result->ocr_runs : NULL; } +const GPtrArray *create_person_dialog_result_get_factual_relations( + const CreatePersonDialogResult *result) +{ + return result != NULL ? result->factual_relations : NULL; +} gboolean create_person_dialog_test_overlay_has_region(GtkWindow *window) { CreatePersonDialogState *state = window != NULL diff --git a/src/views/dialog_geometry.c b/src/views/dialog_geometry.c index fe4ae6b..e42ee2f 100644 --- a/src/views/dialog_geometry.c +++ b/src/views/dialog_geometry.c @@ -10,6 +10,8 @@ typedef struct { double ratio; int minimum_start; int minimum_end; + int previous_width; + guint stable_frames; } LabfyPanedGeometry; LabfyDialogGeometry labfy_dialog_geometry_calculate( @@ -115,19 +117,40 @@ static void paned_geometry_apply( gtk_paned_set_position(paned, position); } -static gboolean paned_geometry_idle(gpointer data) +static gboolean paned_geometry_tick(GtkWidget *widget, GdkFrameClock *clock, + gpointer data) { - GtkPaned *paned = GTK_PANED(data); + GtkPaned *paned = GTK_PANED(widget); LabfyPanedGeometry *geometry = g_object_get_data( G_OBJECT(paned), "labfy-paned-initial-geometry"); + int width; + (void) clock; + (void) data; if (geometry == NULL) return G_SOURCE_REMOVE; - if (gtk_widget_get_width(GTK_WIDGET(paned)) < - geometry->minimum_start + geometry->minimum_end) + width = gtk_widget_get_width(widget); + if (width < geometry->minimum_start + geometry->minimum_end) { + geometry->previous_width = width; + geometry->stable_frames = 0; return G_SOURCE_CONTINUE; + } + if (width != geometry->previous_width) { + geometry->previous_width = width; + geometry->stable_frames = 0; + return G_SOURCE_CONTINUE; + } + if (geometry->stable_frames++ < 1) return G_SOURCE_CONTINUE; paned_geometry_apply(paned, geometry); + g_object_set_data(G_OBJECT(paned), "labfy-paned-initial-ratio-applied", + GINT_TO_POINTER(1)); return G_SOURCE_REMOVE; } +gboolean labfy_paned_initial_ratio_applied(GtkPaned *paned) +{ + return GTK_IS_PANED(paned) && g_object_get_data(G_OBJECT(paned), + "labfy-paned-initial-ratio-applied") != NULL; +} + void labfy_paned_apply_initial_ratio( GtkPaned *paned, double ratio, int minimum_start, int minimum_end) { @@ -142,6 +165,6 @@ void labfy_paned_apply_initial_ratio( geometry->minimum_end = MAX(0, minimum_end); g_object_set_data_full(G_OBJECT(paned), "labfy-paned-initial-geometry", geometry, g_free); - g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, paned_geometry_idle, - g_object_ref(paned), g_object_unref); + gtk_widget_add_tick_callback(GTK_WIDGET(paned), paned_geometry_tick, + NULL, NULL); } diff --git a/src/views/evidence_metadata_dialog.c b/src/views/evidence_metadata_dialog.c index f4a0cd0..97d02c1 100644 --- a/src/views/evidence_metadata_dialog.c +++ b/src/views/evidence_metadata_dialog.c @@ -8,7 +8,10 @@ #include "models/evidence_type.h" #include "dao/identity_ocr_dao.h" +#include "dao/identity_traceability_dao.h" +#include "dao/entity_dao.h" #include "views/evidence_identity_ocr_dialog.h" +#include "views/person_vocabulary_adapter.h" #include "widgets/ocr_provenance_overlay.h" struct EvidenceMetadataDialogResult @@ -537,6 +540,61 @@ gboolean evidence_metadata_dialog_present_with_ocr( gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(ocr_scroll), ocr); gtk_box_append(GTK_BOX(main_box), ocr_scroll); } + + if (database != NULL) { + IdentityTraceabilityDao *trace_dao = identity_traceability_dao_new(database); + if (trace_dao != NULL) { + GPtrArray *relations = identity_traceability_dao_list_factual_relations_by_evidence( + trace_dao, evidence_record_get_identifier(record), NULL); + if (relations != NULL && relations->len > 0) { + GtkWidget *rel_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_append(GTK_BOX(rel_box), gtk_label_new("Relations factuelles avec les personnes")); + EntityDao *entity_dao = entity_dao_new(database, NULL); + for (guint i = 0; i < relations->len; i++) { + PersonEvidenceFactualRelation *rel = g_ptr_array_index(relations, i); + const char *person_id = person_evidence_factual_relation_get_person_identifier(rel); + const char *rel_type = person_vocabulary_adapter_relation_label( + person_evidence_factual_relation_get_relation_type(rel)); + const char *note = person_evidence_factual_relation_get_factual_note(rel); + char *designation = NULL; + if (entity_dao != NULL) { + EntityRecord *person = entity_dao_find_by_identifier(entity_dao, person_id, NULL); + if (person != NULL) { + const char *label = entity_record_get_label(person); + const char *value = entity_record_get_value(person); + designation = g_strdup(label && label[0] ? label : (value ? value : "")); + entity_record_free(person); + } + } + if (designation == NULL) designation = g_strdup("Personne inconnue"); + const char *run = person_evidence_factual_relation_get_ocr_run_identifier(rel); + char *text = g_strdup_printf("• %s : %s\n Date : %s — Origine : %s%s%s", + designation, rel_type, + person_evidence_factual_relation_get_observed_at(rel), + person_evidence_factual_relation_get_origin(rel), + run ? " — OcrRun : " : "", run ? run : ""); + if (note && note[0]) { + char *tmp = g_strdup_printf("%s\n Note : %s", text, note); + g_free(text); + text = tmp; + } + GtkWidget *label = gtk_label_new(text); + gtk_label_set_xalign(GTK_LABEL(label), 0.0f); + gtk_box_append(GTK_BOX(rel_box), label); + g_free(text); + g_free(designation); + } + if (entity_dao != NULL) entity_dao_free(entity_dao); + GtkWidget *rel_scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(rel_scroll), rel_box); + gtk_widget_set_size_request(rel_scroll, -1, 100); + gtk_box_append(GTK_BOX(main_box), rel_scroll); + g_ptr_array_unref(relations); + } + identity_traceability_dao_free(trace_dao); + } + } + buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_halign(buttons, GTK_ALIGN_END); cancel = gtk_button_new_with_label("Annuler"); diff --git a/src/views/identity_ocr_language_loader.c b/src/views/identity_ocr_language_loader.c new file mode 100644 index 0000000..07c979c --- /dev/null +++ b/src/views/identity_ocr_language_loader.c @@ -0,0 +1,84 @@ +#include "views/identity_ocr_language_loader.h" +#include + +typedef struct { + char *executable; + GWeakRef window; + GWeakRef dropdown; +} IdentityLanguageJob; + +static void identity_language_job_free(gpointer data) +{ + IdentityLanguageJob *job = data; + if (job != NULL) { + g_free(job->executable); + g_weak_ref_clear(&job->window); + g_weak_ref_clear(&job->dropdown); + g_free(job); + } +} + +static void identity_languages_worker(GTask *task, gpointer source, + gpointer task_data, GCancellable *cancellable) +{ + (void)source; (void)cancellable; + IdentityLanguageJob *job = task_data; + gchar *stdout_text = NULL; + gboolean success = g_spawn_command_line_sync( + g_strdup_printf("'%s' --list-langs", job->executable), + &stdout_text, NULL, NULL, NULL); + if (success && stdout_text != NULL) { + GPtrArray *langs = g_ptr_array_new_with_free_func(g_free); + gchar **lines = g_strsplit(stdout_text, "\n", -1); + for (gsize i = 1; lines[i] != NULL; i++) { + gchar *lang = g_strstrip(g_strdup(lines[i])); + if (lang[0] != '\0') g_ptr_array_add(langs, lang); + else g_free(lang); + } + g_strfreev(lines); + g_task_return_pointer(task, langs, (GDestroyNotify)g_ptr_array_unref); + } else { + g_task_return_pointer(task, NULL, NULL); + } + g_free(stdout_text); +} + +static void identity_languages_completed(GObject *source, + GAsyncResult *result, gpointer data) +{ + (void)source; + GTask *task = G_TASK(result); + IdentityLanguageJob *job = data; + GPtrArray *langs = g_task_propagate_pointer(task, NULL); + if (langs != NULL) { + GtkWindow *window = GTK_WINDOW(g_weak_ref_get(&job->window)); + GtkDropDown *dropdown = GTK_DROP_DOWN(g_weak_ref_get(&job->dropdown)); + if (window != NULL && dropdown != NULL) { + GtkStringList *list = gtk_string_list_new(NULL); + gtk_string_list_append(list, "Fra (français)"); + for (guint i = 0; i < langs->len; i++) { + const char *lang = g_ptr_array_index(langs, i); + if (g_strcmp0(lang, "fra") != 0) { + gtk_string_list_append(list, lang); + } + } + gtk_drop_down_set_model(dropdown, G_LIST_MODEL(list)); + } + if (window != NULL) g_object_unref(window); + if (dropdown != NULL) g_object_unref(dropdown); + g_ptr_array_unref(langs); + } +} + +void identity_ocr_language_loader_start(GtkWindow *window, const char *tesseract_path, GtkDropDown *language_dropdown) +{ + if (tesseract_path == NULL || window == NULL || language_dropdown == NULL) return; + IdentityLanguageJob *job = g_new0(IdentityLanguageJob, 1); + job->executable = g_strdup(tesseract_path); + g_weak_ref_init(&job->window, G_OBJECT(window)); + g_weak_ref_init(&job->dropdown, G_OBJECT(language_dropdown)); + GTask *task = g_task_new(NULL, NULL, identity_languages_completed, job); + g_task_set_task_data(task, job, identity_language_job_free); + g_task_run_in_thread(task, identity_languages_worker); + g_object_unref(task); +} diff --git a/src/views/main_window.c b/src/views/main_window.c index 3be02fd..b6d72b8 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -1962,11 +1962,11 @@ void main_window_set_entity_selected_callback(MainWindow *main_window, main_window->entity_selected_callback = callback; main_window->entity_selected_user_data = user_data; } -void main_window_set_person_evidences(MainWindow *main_window, - const GPtrArray *records) -{ - if (main_window == NULL) return; - workspace_set_person_evidences(main_window->workspace, records); +void main_window_set_person_evidences(MainWindow *main_window, const GPtrArray *records) { + if (main_window) workspace_set_person_evidences(main_window->workspace, records); +} +void main_window_set_person_factual_relations(MainWindow *main_window, const GPtrArray *relations, const GPtrArray *evidence_records) { + if (main_window) workspace_set_person_factual_relations(main_window->workspace, relations, evidence_records); } void main_window_set_quit_callback( diff --git a/src/views/person_factual_relation_editor.c b/src/views/person_factual_relation_editor.c new file mode 100644 index 0000000..60afdd1 --- /dev/null +++ b/src/views/person_factual_relation_editor.c @@ -0,0 +1,267 @@ +#include "views/person_factual_relation_editor.h" +#include "views/person_vocabulary_adapter.h" + +typedef struct { + char *evidence_id, *run_id, *type, *note; + GtkDropDown *evidence, *run; +} RelationRow; + +struct PersonFactualRelationEditor { + GtkWidget *root, *empty; + GtkListBox *list; + GPtrArray *rows, *evidence_ids, *runs; + GtkStringList *evidence_labels; +}; + +static GQuark editor_error(void) +{ return g_quark_from_static_string("person-factual-relation-editor"); } +static void row_free(gpointer data) +{ + RelationRow *row=data; + if(!row)return; + g_free(row->evidence_id);g_free(row->run_id);g_free(row->type); + g_free(row->note);g_free(row); +} +static void input_free(gpointer data) +{ + PersonCreationFactualRelationInput *input=data; + if(!input)return; + g_free((char*)input->evidence_selection_identifier); + g_free((char*)input->ocr_run_identifier); + g_free((char*)input->relation_type);g_free((char*)input->factual_note); + g_free(input); +} +static void update_empty(PersonFactualRelationEditor *editor) +{ + gboolean empty=editor->rows->len==0; + gtk_widget_set_visible(editor->empty,empty); + gtk_widget_set_visible(GTK_WIDGET(editor->list),!empty); +} +static GtkStringList *evidence_model(PersonFactualRelationEditor *editor) +{ + GtkStringList *model=gtk_string_list_new(NULL); + gtk_string_list_append(model,"Sélectionner une preuve…"); + for(guint i=0;editor->evidence_labels&& + ievidence_labels));i++) + gtk_string_list_append(model, + gtk_string_list_get_string(editor->evidence_labels,i)); + return model; +} +static GtkStringList *run_model(PersonFactualRelationEditor *editor, + const char *evidence_id) +{ + GtkStringList *model=gtk_string_list_new(NULL); + gtk_string_list_append(model,"Aucun OcrRun (facultatif)"); + for(guint i=0;editor->runs&&iruns->len;i++){ + IdentityOcrRun *run=g_ptr_array_index(editor->runs,i); + if(g_strcmp0(identity_ocr_run_get_evidence_id(run),evidence_id)!=0) + continue; + char *label=g_strdup_printf("OcrRun %s", + identity_ocr_run_get_identifier(run)); + gtk_string_list_append(model,label);g_free(label); + } + return model; +} +static const char *filtered_run_id(PersonFactualRelationEditor *editor, + const char *evidence_id,guint selected) +{ + if(selected==0)return NULL; + guint position=1; + for(guint i=0;editor->runs&&iruns->len;i++){ + IdentityOcrRun *run=g_ptr_array_index(editor->runs,i); + if(g_strcmp0(identity_ocr_run_get_evidence_id(run),evidence_id)!=0) + continue; + if(position++==selected)return identity_ocr_run_get_identifier(run); + } + return NULL; +} +static void evidence_changed(GtkDropDown *drop,GParamSpec *pspec,gpointer data) +{ + (void)pspec; RelationRow *row=data; + PersonFactualRelationEditor *editor=g_object_get_data(G_OBJECT(drop),"editor"); + guint selected=gtk_drop_down_get_selected(drop); + g_free(row->evidence_id);row->evidence_id=NULL; + if(selected>0&&editor->evidence_ids&&selected<=editor->evidence_ids->len) + row->evidence_id=g_strdup(g_ptr_array_index(editor->evidence_ids,selected-1)); + g_clear_pointer(&row->run_id,g_free); + GtkStringList *model=run_model(editor,row->evidence_id); + gtk_drop_down_set_model(row->run,G_LIST_MODEL(model)); + gtk_drop_down_set_selected(row->run,0); +} +static void type_changed(GtkDropDown *drop,GParamSpec *pspec,gpointer data) +{ + (void)pspec;RelationRow *row=data;guint selected=gtk_drop_down_get_selected(drop); + g_free(row->type);row->type=selected?g_strdup( + person_vocabulary_adapter_relation_code(selected-1)):NULL; +} +static void run_changed(GtkDropDown *drop,GParamSpec *pspec,gpointer data) +{ + (void)pspec;RelationRow *row=data; + PersonFactualRelationEditor *editor=g_object_get_data(G_OBJECT(drop),"editor"); + g_free(row->run_id);row->run_id=g_strdup(filtered_run_id(editor, + row->evidence_id,gtk_drop_down_get_selected(drop))); +} +static void note_changed(GtkEditable *editable,gpointer data) +{ + RelationRow *row=data;g_free(row->note); + row->note=g_strdup(gtk_editable_get_text(editable)); +} +static void remove_clicked(GtkButton *button,gpointer data) +{ + PersonFactualRelationEditor *editor=data; + GtkWidget *row=gtk_widget_get_ancestor(GTK_WIDGET(button),GTK_TYPE_LIST_BOX_ROW); + gint index=row?gtk_list_box_row_get_index(GTK_LIST_BOX_ROW(row)):-1; + if(index<0||(guint)index>=editor->rows->len)return; + gtk_list_box_remove(editor->list,row); + g_ptr_array_remove_index(editor->rows,(guint)index);update_empty(editor); +} +static void add_clicked(GtkButton *button,gpointer data) +{ + (void)button;PersonFactualRelationEditor *editor=data; + RelationRow *state=g_new0(RelationRow,1); + GtkWidget *row=gtk_list_box_row_new(),*box=gtk_box_new(GTK_ORIENTATION_VERTICAL,6); + GtkWidget *header=gtk_box_new(GTK_ORIENTATION_HORIZONTAL,6); + GtkStringList *types=person_vocabulary_adapter_create_relation_labels(); + const char *placeholder[]={"Sélectionner un type…",NULL}; + gtk_string_list_splice(types,0,0,placeholder); + GtkWidget *type=gtk_drop_down_new(G_LIST_MODEL(types),NULL); + GtkWidget *remove=gtk_button_new_from_icon_name("list-remove-symbolic"); + gtk_widget_set_name(type,"factual-relation-type"); + gtk_widget_set_name(remove,"factual-relation-remove"); + gtk_widget_set_tooltip_text(remove,"Retirer cette relation préparée"); + gtk_widget_set_hexpand(type,TRUE);gtk_box_append(GTK_BOX(header),type); + gtk_box_append(GTK_BOX(header),remove);gtk_box_append(GTK_BOX(box),header); + GtkStringList *evidences=evidence_model(editor); + state->evidence=GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(evidences),NULL)); + gtk_widget_set_name(GTK_WIDGET(state->evidence),"factual-relation-evidence"); + gtk_box_append(GTK_BOX(box),GTK_WIDGET(state->evidence)); + GtkStringList *runs=run_model(editor,NULL); + state->run=GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(runs),NULL)); + gtk_widget_set_name(GTK_WIDGET(state->run),"factual-relation-ocr-run"); + gtk_box_append(GTK_BOX(box),GTK_WIDGET(state->run)); + GtkWidget *note=gtk_entry_new();gtk_entry_set_placeholder_text(GTK_ENTRY(note), + "Note factuelle (facultative)");gtk_box_append(GTK_BOX(box),note); + gtk_widget_set_name(note,"factual-relation-note"); + gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row),box); + gtk_list_box_append(editor->list,row);g_ptr_array_add(editor->rows,state); + g_object_set_data(G_OBJECT(state->evidence),"editor",editor); + g_object_set_data(G_OBJECT(state->run),"editor",editor); + g_signal_connect(type,"notify::selected",G_CALLBACK(type_changed),state); + g_signal_connect(state->evidence,"notify::selected",G_CALLBACK(evidence_changed),state); + g_signal_connect(state->run,"notify::selected",G_CALLBACK(run_changed),state); + g_signal_connect(note,"changed",G_CALLBACK(note_changed),state); + g_signal_connect(remove,"clicked",G_CALLBACK(remove_clicked),editor);update_empty(editor); +} +PersonFactualRelationEditor *person_factual_relation_editor_new(void) +{ + PersonFactualRelationEditor *editor=g_new0(PersonFactualRelationEditor,1); + editor->rows=g_ptr_array_new_with_free_func(row_free); + editor->root=gtk_box_new(GTK_ORIENTATION_VERTICAL,8); + GtkWidget *header=gtk_box_new(GTK_ORIENTATION_HORIZONTAL,8); + GtkWidget *title=gtk_label_new("Relations factuelles"); + gtk_widget_set_hexpand(title,TRUE);gtk_widget_set_halign(title,GTK_ALIGN_START); + GtkWidget *add=gtk_button_new_with_label("Ajouter une relation"); + gtk_widget_set_name(add,"factual-relation-add"); + gtk_box_append(GTK_BOX(header),title);gtk_box_append(GTK_BOX(header),add); + gtk_box_append(GTK_BOX(editor->root),header); + editor->list=GTK_LIST_BOX(gtk_list_box_new()); + gtk_list_box_set_selection_mode(editor->list,GTK_SELECTION_NONE); + editor->empty=gtk_label_new("Aucune relation factuelle préparée."); + gtk_box_append(GTK_BOX(editor->root),GTK_WIDGET(editor->list)); + gtk_box_append(GTK_BOX(editor->root),editor->empty); + g_signal_connect(add,"clicked",G_CALLBACK(add_clicked),editor);update_empty(editor); + return editor; +} +GtkWidget *person_factual_relation_editor_get_widget(PersonFactualRelationEditor *editor) +{return editor?editor->root:NULL;} +void person_factual_relation_editor_set_available_evidence( + PersonFactualRelationEditor *editor,GtkStringList *labels,GPtrArray *ids) +{ + if(!editor)return; + g_set_object(&editor->evidence_labels,labels); + if(editor->evidence_ids)g_ptr_array_unref(editor->evidence_ids); + editor->evidence_ids=ids?g_ptr_array_ref(ids):NULL; + for(guint i=0;irows->len;i++){ + RelationRow *row=g_ptr_array_index(editor->rows,i); + GtkStringList *model=evidence_model(editor); + gtk_drop_down_set_model(row->evidence,G_LIST_MODEL(model)); + gtk_drop_down_set_selected(row->evidence,0); + } +} +void person_factual_relation_editor_set_available_ocr_runs( + PersonFactualRelationEditor *editor,GPtrArray *runs) +{ + if(!editor)return; + if(editor->runs)g_ptr_array_unref(editor->runs); + editor->runs=runs?g_ptr_array_ref(runs):NULL; + for(guint i=0;irows->len;i++){ + RelationRow *row=g_ptr_array_index(editor->rows,i); + GtkStringList *model=run_model(editor,row->evidence_id); + gtk_drop_down_set_model(row->run,G_LIST_MODEL(model)); + g_clear_pointer(&row->run_id,g_free);gtk_drop_down_set_selected(row->run,0); + } +} +gboolean person_factual_relation_editor_collect_relations( + PersonFactualRelationEditor *editor,GPtrArray **relations,GError **error) +{ + if(!editor||!relations){g_set_error_literal(error,editor_error(),1, + "Éditeur de relations factuelles invalide.");return FALSE;} + GPtrArray *result=g_ptr_array_new_with_free_func(input_free); + GHashTable *keys=g_hash_table_new_full(g_str_hash,g_str_equal,g_free,NULL); + for(guint i=0;irows->len;i++){ + RelationRow *row=g_ptr_array_index(editor->rows,i);char *note=g_strdup(row->note); + if(note)g_strstrip(note); + if(note&&!*note)g_clear_pointer(¬e,g_free); + if(!row->evidence_id||!row->type){g_free(note);g_set_error_literal(error, + editor_error(),2,"Chaque relation doit avoir une preuve et un type explicites.");goto bad;} + char *key=g_strdup_printf("%s\037%s\037%s\037%s",row->evidence_id,row->type, + row->run_id?row->run_id:"",note?note:""); + if(g_hash_table_contains(keys,key)){g_free(key);g_free(note); + g_set_error_literal(error,editor_error(),3, + "Cette relation factuelle est déjà préparée.");goto bad;} + g_hash_table_add(keys,key); + PersonCreationFactualRelationInput *input=g_new0(PersonCreationFactualRelationInput,1); + input->evidence_selection_identifier=g_strdup(row->evidence_id); + input->ocr_run_identifier=g_strdup(row->run_id);input->relation_type=g_strdup(row->type); + input->factual_note=note;g_ptr_array_add(result,input); + } + g_hash_table_unref(keys);*relations=result;return TRUE; +bad:g_hash_table_unref(keys);g_ptr_array_unref(result);return FALSE; +} +guint person_factual_relation_editor_get_count(const PersonFactualRelationEditor *editor) +{return editor?editor->rows->len:0;} +gboolean person_factual_relation_editor_validate( + PersonFactualRelationEditor *editor,GError **error) +{ + GPtrArray *relations=NULL; + gboolean valid=person_factual_relation_editor_collect_relations( + editor,&relations,error); + if(relations)g_ptr_array_unref(relations); + return valid; +} +void person_factual_relation_editor_append_summary( + PersonFactualRelationEditor *editor,GString *summary) +{ + GPtrArray *relations=NULL; + if(!summary||!person_factual_relation_editor_collect_relations( + editor,&relations,NULL))return; + g_string_append(summary,"\n\nRelations factuelles préparées"); + if(relations->len==0)g_string_append(summary,"\nAucune relation."); + for(guint i=0;ilen;i++){ + PersonCreationFactualRelationInput *relation=g_ptr_array_index(relations,i); + g_string_append_printf(summary,"\n• %s — preuve %s%s%s", + person_vocabulary_adapter_relation_label(relation->relation_type), + relation->evidence_selection_identifier, + relation->factual_note?" — ":"",relation->factual_note?relation->factual_note:""); + } + g_ptr_array_unref(relations); +} +void person_factual_relation_editor_free(PersonFactualRelationEditor *editor) +{ + if(!editor)return; + g_ptr_array_unref(editor->rows); + g_clear_object(&editor->evidence_labels); + if(editor->evidence_ids)g_ptr_array_unref(editor->evidence_ids); + if(editor->runs)g_ptr_array_unref(editor->runs); + g_free(editor); +} diff --git a/src/views/person_vocabulary_adapter.c b/src/views/person_vocabulary_adapter.c index 0e8e1ef..1c4c112 100644 --- a/src/views/person_vocabulary_adapter.c +++ b/src/views/person_vocabulary_adapter.c @@ -63,6 +63,59 @@ GtkStringList *person_vocabulary_adapter_create_status_labels( const PersonVocabularyAdapter *adapter) { return create_labels(person_vocabulary_adapter_get_statuses(adapter)); } +static const char *const factual_relation_types[] = { + "identity_observed_in", + "document_presented_in_name_of", + "declared_holder_in", + "data_extracted_from" +}; + +static const char *const factual_relation_labels[] = { + "Identité observée dans la preuve", + "Document présenté au nom de la personne", + "Titulaire déclaré dans la preuve", + "Donnée extraite de la preuve" +}; +static const char *const factual_relation_descriptions[] = { + "Une identité est visible ou mentionnée dans cette preuve.", + "Le document a été présenté au nom de cette personne, sans conclure à son authenticité.", + "La preuve déclare cette personne comme titulaire, sans vérification automatique.", + "Une donnée concernant cette personne a été extraite de cette preuve." +}; + +GtkStringList *person_vocabulary_adapter_create_relation_labels(void) +{ + GtkStringList *labels = gtk_string_list_new(NULL); + for (guint i = 0; i < 4; i++) { + gtk_string_list_append(labels, factual_relation_labels[i]); + } + return labels; +} + +const char *person_vocabulary_adapter_relation_code(guint index) +{ + if (index < 4) return factual_relation_types[index]; + return NULL; +} + +const char *person_vocabulary_adapter_relation_label(const char *code) +{ + if (code == NULL) return "Inconnu"; + for (guint i = 0; i < 4; i++) { + if (g_strcmp0(code, factual_relation_types[i]) == 0) + return factual_relation_labels[i]; + } + return "Inconnu"; +} +const char *person_vocabulary_adapter_relation_description(const char *code) +{ + if (code == NULL) return NULL; + for (guint i = 0; i < G_N_ELEMENTS(factual_relation_types); i++) + if (g_strcmp0(code, factual_relation_types[i]) == 0) + return factual_relation_descriptions[i]; + return NULL; +} + const char *person_vocabulary_adapter_status_code( const PersonVocabularyAdapter *adapter, guint index) { diff --git a/src/widgets/entity_details_panel.c b/src/widgets/entity_details_panel.c index d25294c..bf5f58d 100644 --- a/src/widgets/entity_details_panel.c +++ b/src/widgets/entity_details_panel.c @@ -7,6 +7,8 @@ #include "models/entity_record.h" #include "models/evidence_record.h" +#include "models/identity_traceability.h" +#include "views/person_vocabulary_adapter.h" #include @@ -39,6 +41,7 @@ struct EntityDetailsPanel GtkWidget *person_name_button; GtkWidget *person_evidence_button; GtkWidget *person_evidence_summary_label; + GtkWidget *person_factual_relations_box; GtkWidget *entity_evidence_box; GtkWidget *evidence_buttons_box; @@ -885,6 +888,8 @@ EntityDetailsPanel *entity_details_panel_new(void) gtk_box_append(GTK_BOX(details_panel->entity_evidence_box), details_panel->person_evidence_button); gtk_box_append(GTK_BOX(details_box), details_panel->person_role_box); + details_panel->person_factual_relations_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_append(GTK_BOX(details_box), details_panel->person_factual_relations_box); gtk_box_append(GTK_BOX(details_box), details_panel->entity_evidence_box); g_signal_connect(details_panel->person_role_dropdown, "notify::selected", G_CALLBACK(entity_details_panel_on_person_role_changed), details_panel); @@ -1390,6 +1395,63 @@ void entity_details_panel_set_person_evidences(EntityDetailsPanel *panel, } } +void entity_details_panel_set_person_factual_relations( + EntityDetailsPanel *panel, const GPtrArray *relations, const GPtrArray *evidence_records) +{ + GtkWidget *child = NULL; + if (panel == NULL || panel->person_factual_relations_box == NULL) return; + while ((child = gtk_widget_get_first_child(panel->person_factual_relations_box)) != NULL) + gtk_box_remove(GTK_BOX(panel->person_factual_relations_box), child); + + if (relations == NULL || relations->len == 0) { + gtk_widget_set_visible(panel->person_factual_relations_box, FALSE); + return; + } + + gtk_widget_set_visible(panel->person_factual_relations_box, TRUE); + GtkWidget *title = gtk_label_new("Relations factuelles dans les preuves"); + gtk_label_set_wrap(GTK_LABEL(title), TRUE); + gtk_label_set_xalign(GTK_LABEL(title), 0.0f); + gtk_box_append(GTK_BOX(panel->person_factual_relations_box), title); + + for (guint i = 0; i < relations->len; i++) { + PersonEvidenceFactualRelation *rel = g_ptr_array_index(relations, i); + const char *evidence_id = person_evidence_factual_relation_get_evidence_identifier(rel); + const char *rel_type = person_vocabulary_adapter_relation_label( + person_evidence_factual_relation_get_relation_type(rel)); + const char *note = person_evidence_factual_relation_get_factual_note(rel); + + const char *evidence_name = "Preuve inconnue"; + if (evidence_records != NULL) { + for (guint j = 0; j < evidence_records->len; j++) { + const EvidenceRecord *er = g_ptr_array_index(evidence_records, j); + if (g_strcmp0(evidence_record_get_identifier(er), evidence_id) == 0) { + evidence_name = evidence_record_get_original_name(er); + break; + } + } + } + + const char *run = person_evidence_factual_relation_get_ocr_run_identifier(rel); + char *text = g_strdup_printf("• %s : %s\n Date : %s — Origine : %s%s%s", + evidence_name, rel_type, + person_evidence_factual_relation_get_observed_at(rel), + person_evidence_factual_relation_get_origin(rel), + run ? " — OcrRun : " : "", run ? run : ""); + if (note && note[0]) { + char *tmp = g_strdup_printf("%s\n Note : %s", text, note); + g_free(text); + text = tmp; + } + + GtkWidget *label = gtk_label_new(text); + gtk_label_set_xalign(GTK_LABEL(label), 0.0f); + gtk_label_set_wrap(GTK_LABEL(label), TRUE); + gtk_box_append(GTK_BOX(panel->person_factual_relations_box), label); + g_free(text); + } +} + gboolean entity_details_panel_is_open( const EntityDetailsPanel *details_panel ) diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index a5c3e91..2d8a7dc 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -3911,12 +3911,11 @@ void workspace_set_entity_selected_callback(Workspace *workspace, workspace->entity_selected_callback = callback; workspace->entity_selected_user_data = user_data; } -void workspace_set_person_evidences(Workspace *workspace, - const GPtrArray *records) -{ - if (workspace == NULL) return; - entity_details_panel_set_person_evidences( - workspace->entity_details_panel, records); +void workspace_set_person_evidences(Workspace *workspace, const GPtrArray *records) { + if (workspace) entity_details_panel_set_person_evidences(workspace->entity_details_panel, records); +} +void workspace_set_person_factual_relations(Workspace *workspace, const GPtrArray *relations, const GPtrArray *evidence_records) { + if (workspace) entity_details_panel_set_person_factual_relations(workspace->entity_details_panel, relations, evidence_records); } void workspace_reset_graph_layout( diff --git a/tests/test_create_person_dialog_gtk.c b/tests/test_create_person_dialog_gtk.c index 186b1ed..cac8928 100644 --- a/tests/test_create_person_dialog_gtk.c +++ b/tests/test_create_person_dialog_gtk.c @@ -222,6 +222,9 @@ static gboolean click_cancel(gpointer data) g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), "L’OCR produit des propositions à vérifier")); g_signal_emit_by_name(button, "clicked"); + g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), + "Relations factuelles")); + g_signal_emit_by_name(button, "clicked"); g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), "Désignation : SPECIMEN")); g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), diff --git a/tests/test_create_person_dialog_ocr_gtk.c b/tests/test_create_person_dialog_ocr_gtk.c index b6db498..16a1f82 100644 --- a/tests/test_create_person_dialog_ocr_gtk.c +++ b/tests/test_create_person_dialog_ocr_gtk.c @@ -570,6 +570,8 @@ static gboolean drive(gpointer data) context->confirmation_height=gtk_widget_get_height( GTK_WIDGET(context->dialog)); g_signal_emit_by_name(find_button(root,"Suivant"),"clicked"); + g_assert_nonnull(find_label(root,"Relations factuelles")); + g_signal_emit_by_name(find_button(root,"Suivant"),"clicked"); g_assert_nonnull(find_label(root,"page 2")); g_assert_nonnull(find_label(root,"PAGE 2 VALEUR FINALE")); g_assert_nonnull(find_label(root,"authenticité n’est pas établie")); diff --git a/tests/test_evidence_identity_import_gtk.c b/tests/test_evidence_identity_import_gtk.c index 62950a5..fd00e78 100644 --- a/tests/test_evidence_identity_import_gtk.c +++ b/tests/test_evidence_identity_import_gtk.c @@ -10,6 +10,7 @@ #include "models/evidence_type.h" #include "views/evidence_identity_ocr_dialog.h" #include "views/evidence_import_dialog.h" +#include "views/dialog_geometry.h" #include "views/main_window.h" #include "widgets/workspace.h" #include @@ -497,7 +498,7 @@ static gboolean drive(gpointer data) g_assert_nonnull(gtk_paned_get_end_child(GTK_PANED(paned))); g_assert_false(is_descendant_of(actions, form_scroll)); if (gtk_widget_get_width(paned) <= 0 || - gtk_paned_get_position(GTK_PANED(paned)) <= 0) + !labfy_paned_initial_ratio_applied(GTK_PANED(paned))) return G_SOURCE_CONTINUE; if (!context->layout_resize_pending) { int initial_position = @@ -564,12 +565,8 @@ static gboolean drive(gpointer data) context, "Import — OCR d’identité facultatif"); if (dialog == NULL) return G_SOURCE_CONTINUE; GtkWidget *root = GTK_WIDGET(dialog); - int default_width = 0; - int default_height = 0; - gtk_window_get_default_size( - dialog, &default_width, &default_height); - g_assert_cmpint(default_width, ==, 760); - g_assert_cmpint(default_height, ==, 560); + g_assert_cmpint(gtk_widget_get_width(GTK_WIDGET(dialog)), >=, 800); + g_assert_cmpint(gtk_widget_get_height(GTK_WIDGET(dialog)), >=, 600); GtkWidget *continue_button = find_button(root, "Continuer l’import avec OCR"); if (!gtk_widget_get_sensitive(continue_button)) diff --git a/tests/test_identity_traceability.c b/tests/test_identity_traceability.c index 8ecb323..208bda4 100644 --- a/tests/test_identity_traceability.c +++ b/tests/test_identity_traceability.c @@ -66,6 +66,16 @@ static void fixture_free(Fixture*f) static void test_models(void) { + static const char *codes[]={"identity_observed_in", + "document_presented_in_name_of","declared_holder_in","data_extracted_from"}; + static const char *labels[]={"Identité observée dans la preuve", + "Document présenté au nom de la personne","Titulaire déclaré dans la preuve", + "Donnée extraite de la preuve"}; + for(guint i=0;ilen,==,1);g_ptr_array_unref(relations); + relations=identity_traceability_dao_list_factual_relations_by_person(d,PERSON,&e); + g_assert_cmpuint(relations->len,==,1);g_ptr_array_unref(relations); + PersonEvidenceFactualRelation*foreign=person_evidence_factual_relation_new( + "70000000-0000-4000-8000-000000000019",PERSON, + "10000000-0000-4000-8000-000000000019",RUN,"data_extracted_from", + "Run étranger SPECIMEN",AT,TRUE); + g_assert_false(identity_traceability_dao_insert_factual_relation(d,foreign,&e)); + g_assert_error(e,g_quark_from_static_string("identity-traceability-dao"),1); + g_assert_nonnull(strstr(e->message,"ne correspond pas"));g_clear_error(&e); + person_evidence_factual_relation_free(foreign); GPtrArray*roles=identity_traceability_dao_list_roles(d,TRUE,&e); g_assert_cmpuint(roles->len,>=,14);g_ptr_array_unref(roles); roles=identity_traceability_dao_list_roles(d,FALSE,&e); diff --git a/tests/test_person_factual_relation_editor_gtk.c b/tests/test_person_factual_relation_editor_gtk.c new file mode 100644 index 0000000..5e164b2 --- /dev/null +++ b/tests/test_person_factual_relation_editor_gtk.c @@ -0,0 +1,62 @@ +#include "views/person_factual_relation_editor.h" +#include + +static GtkWidget *find_named(GtkWidget *widget,const char *name) +{ + if(g_strcmp0(gtk_widget_get_name(widget),name)==0)return widget; + for(GtkWidget *child=gtk_widget_get_first_child(widget);child; + child=gtk_widget_get_next_sibling(child)){ + GtkWidget *found=find_named(child,name);if(found)return found; + } + return NULL; +} +static void click_named(GtkWidget *root,const char *name) +{g_signal_emit_by_name(find_named(root,name),"clicked");} +static void test_editor(void) +{ + const char *evidence_a="10000000-0000-4000-8000-000000000001"; + const char *evidence_b="10000000-0000-4000-8000-000000000002"; + GtkStringList *labels=gtk_string_list_new((const char*[]){"Preuve A","Preuve B",NULL}); + GPtrArray *ids=g_ptr_array_new_with_free_func(g_free); + g_ptr_array_add(ids,g_strdup(evidence_a));g_ptr_array_add(ids,g_strdup(evidence_b)); + GPtrArray *runs=g_ptr_array_new_with_free_func((GDestroyNotify)identity_ocr_run_free); + g_ptr_array_add(runs,identity_ocr_run_new(evidence_a, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "identity_card","front",1,"fra","none")); + g_ptr_array_add(runs,identity_ocr_run_new(evidence_b, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "passport","identity_page",1,"fra","none")); + PersonFactualRelationEditor *editor=person_factual_relation_editor_new(); + person_factual_relation_editor_set_available_evidence(editor,labels,ids); + person_factual_relation_editor_set_available_ocr_runs(editor,runs); + GtkWidget *root=person_factual_relation_editor_get_widget(editor); + GtkWindow *window=GTK_WINDOW(gtk_window_new()); + gtk_window_set_child(window,root);gtk_window_present(window); + GPtrArray *relations=NULL;GError *error=NULL; + g_assert_true(person_factual_relation_editor_collect_relations(editor,&relations,&error)); + g_assert_cmpuint(relations->len,==,0);g_ptr_array_unref(relations); + click_named(root,"factual-relation-add"); + g_assert_cmpuint(person_factual_relation_editor_get_count(editor),==,1); + gtk_drop_down_set_selected(GTK_DROP_DOWN(find_named(root,"factual-relation-type")),1); + gtk_drop_down_set_selected(GTK_DROP_DOWN(find_named(root,"factual-relation-evidence")),1); + GtkDropDown *run=GTK_DROP_DOWN(find_named(root,"factual-relation-ocr-run")); + g_assert_cmpuint(g_list_model_get_n_items(gtk_drop_down_get_model(run)),==,2); + gtk_drop_down_set_selected(run,1); + gtk_editable_set_text(GTK_EDITABLE(find_named(root,"factual-relation-note"))," Note SPECIMEN "); + g_assert_true(person_factual_relation_editor_collect_relations(editor,&relations,&error)); + g_assert_cmpuint(relations->len,==,1); + PersonCreationFactualRelationInput *input=g_ptr_array_index(relations,0); + g_assert_cmpstr(input->factual_note,==,"Note SPECIMEN");g_ptr_array_unref(relations); + click_named(root,"factual-relation-remove"); + g_assert_cmpuint(person_factual_relation_editor_get_count(editor),==,0); + gtk_window_destroy(window);while(g_main_context_iteration(NULL,FALSE)); + person_factual_relation_editor_free(editor);g_ptr_array_unref(runs); + g_ptr_array_unref(ids);g_object_unref(labels); +} +int main(int argc,char **argv) +{ + if(!gtk_init_check()){g_print("SKIP: aucun affichage GTK disponible.\n");return 0;} + g_test_init(&argc,&argv,NULL); + g_test_add_func("/factual-relation-editor/explicit-filter-remove",test_editor); + return g_test_run(); +} diff --git a/wc_out.txt b/wc_out.txt new file mode 100644 index 0000000..7ddf263 --- /dev/null +++ b/wc_out.txt @@ -0,0 +1,10 @@ + 9339 src/core/application.c + 142 src/core/person_creation_task.c + 2037 src/views/create_person_dialog.c + 660 src/views/evidence_metadata_dialog.c + 2245 src/views/main_window.c + 286 src/views/person_factual_relation_editor.c + 193 src/views/person_vocabulary_adapter.c + 1550 src/widgets/entity_details_panel.c + 4078 src/widgets/workspace.c + 20530 total