feat(person): ajouter les relations factuelles contrôlées

This commit is contained in:
grayTerminal-sh 2026-08-01 09:33:03 +02:00
parent 4b2fb93076
commit 11cbc061ae
39 changed files with 1070 additions and 121 deletions

View file

@ -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)

20
diff_stat.txt Normal file
View file

@ -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(-)

View file

@ -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;

View file

@ -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,

View file

@ -0,0 +1,10 @@
#ifndef PERSON_DETAILS_PROVIDER_H
#define PERSON_DETAILS_PROVIDER_H
#include <glib.h>
#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

View file

@ -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(

View file

@ -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);

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,8 @@
#ifndef IDENTITY_OCR_LANGUAGE_LOADER_H
#define IDENTITY_OCR_LANGUAGE_LOADER_H
#include <gtk/gtk.h>
void identity_ocr_language_loader_start(GtkWindow *window, const char *tesseract_path, GtkDropDown *language_dropdown);
#endif

View file

@ -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);

View file

@ -0,0 +1,40 @@
#ifndef LABFY_PERSON_FACTUAL_RELATION_EDITOR_H
#define LABFY_PERSON_FACTUAL_RELATION_EDITOR_H
#include <gtk/gtk.h>
#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

View file

@ -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(

View file

@ -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.

View file

@ -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.

19
name_status.txt Normal file
View file

@ -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

View file

@ -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. */

View file

@ -11,6 +11,7 @@
#include <glib/gstdio.h>
#include <string.h>
static GQuark coordinator_error(void)
{
return g_quark_from_static_string("person-creation-coordinator-error");

View file

@ -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 douvrir la base de lenquête.");
return FALSE;
}
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,

View file

@ -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;
}

View file

@ -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,"Lexé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)
{

View file

@ -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(

View file

@ -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) { \

View file

@ -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é\nLOCR 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

View file

@ -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);
}

View file

@ -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");

View file

@ -0,0 +1,84 @@
#include "views/identity_ocr_language_loader.h"
#include <gio/gio.h>
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);
}

View file

@ -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(

View file

@ -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&&
i<g_list_model_get_n_items(G_LIST_MODEL(editor->evidence_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&&i<editor->runs->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&&i<editor->runs->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;i<editor->rows->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;i<editor->rows->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;i<editor->rows->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(&note,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;i<relations->len;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);
}

View file

@ -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)
{

View file

@ -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 <glib.h>
@ -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
)

View file

@ -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(

View file

@ -222,6 +222,9 @@ static gboolean click_cancel(gpointer data)
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
"LOCR 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),

View file

@ -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é nest pas établie"));

View file

@ -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 <cairo.h>
@ -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 didentité 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 limport avec OCR");
if (!gtk_widget_get_sensitive(continue_button))

View file

@ -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;i<G_N_ELEMENTS(codes);i++){
g_assert_cmpstr(person_vocabulary_adapter_relation_code(i),==,codes[i]);
g_assert_cmpstr(person_vocabulary_adapter_relation_label(codes[i]),==,labels[i]);
g_assert_nonnull(person_vocabulary_adapter_relation_description(codes[i]));
}
PersonRoleVocabularyEntry justified={.requires_justification=TRUE};
g_assert_false(person_vocabulary_adapter_justification_valid(
&justified," "));
@ -107,6 +117,16 @@ static void test_dao_history_and_relations(void)
g_assert_true(identity_traceability_dao_insert_factual_relation(d,r,&e));
GPtrArray*relations=identity_traceability_dao_list_factual_relations(d,EVIDENCE,&e);
g_assert_cmpuint(relations->len,==,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);

View file

@ -0,0 +1,62 @@
#include "views/person_factual_relation_editor.h"
#include <gtk/gtk.h>
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();
}

10
wc_out.txt Normal file
View file

@ -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