feat(person): intégrer la traçabilité V18 atomique
This commit is contained in:
parent
d7131aa1b6
commit
4b2fb93076
38 changed files with 1698 additions and 148 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,6 +5,7 @@ labfy-investigation
|
||||||
|
|
||||||
# Executables de tests
|
# Executables de tests
|
||||||
/tests/test_*
|
/tests/test_*
|
||||||
|
/tests/fake_document_tool
|
||||||
|
|
||||||
# Conserver les sources des tests
|
# Conserver les sources des tests
|
||||||
!/tests/test_*.c
|
!/tests/test_*.c
|
||||||
|
|
|
||||||
57
Makefile
57
Makefile
|
|
@ -8,6 +8,36 @@ endif
|
||||||
|
|
||||||
.DEFAULT_GOAL := all
|
.DEFAULT_GOAL := all
|
||||||
|
|
||||||
|
SOURCE_SIZE_LIMIT := 2000
|
||||||
|
# Exceptions historiques : plafonds constatés avant la tranche V18.
|
||||||
|
SOURCE_SIZE_EXCEPTIONS := \
|
||||||
|
src/core/application.c:9325 \
|
||||||
|
src/widgets/investigation_graph_view.c:4901 \
|
||||||
|
src/widgets/workspace.c:4071 \
|
||||||
|
src/views/main_window.c:2239 \
|
||||||
|
src/views/create_relation_dialog.c:2043
|
||||||
|
|
||||||
|
.PHONY: check-source-size
|
||||||
|
check-source-size:
|
||||||
|
@limit=$(SOURCE_SIZE_LIMIT); failed=0; \
|
||||||
|
files="$$(git ls-files --cached --others --exclude-standard -- \
|
||||||
|
'src/*.c' 'src/**/*.c' | sort -u)"; \
|
||||||
|
for file in $$files; do \
|
||||||
|
lines=$$(wc -l < "$$file"); allowed=$$limit; historical=0; \
|
||||||
|
for exception in $(SOURCE_SIZE_EXCEPTIONS); do \
|
||||||
|
case "$$exception" in "$$file":*) \
|
||||||
|
allowed=$${exception##*:}; historical=1;; esac; \
|
||||||
|
done; \
|
||||||
|
if [ $$historical -eq 1 ]; then \
|
||||||
|
echo "EXCEPTION HISTORIQUE $$file : $$lines/$$allowed lignes (limite normale $$limit)"; \
|
||||||
|
fi; \
|
||||||
|
if [ $$lines -gt $$allowed ]; then \
|
||||||
|
echo "ERREUR taille source $$file : $$lines lignes, maximum $$allowed"; \
|
||||||
|
failed=1; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
exit $$failed
|
||||||
|
|
||||||
CFLAGS = -std=c17 \
|
CFLAGS = -std=c17 \
|
||||||
-Wall \
|
-Wall \
|
||||||
-Wextra \
|
-Wextra \
|
||||||
|
|
@ -160,6 +190,7 @@ TEST_PDF_ANALYSIS := tests/test_pdf_analysis
|
||||||
TEST_DOCUMENT_TOOL_RUNNER := tests/test_document_tool_runner
|
TEST_DOCUMENT_TOOL_RUNNER := tests/test_document_tool_runner
|
||||||
TEST_IDENTITY_OCR := tests/test_identity_ocr
|
TEST_IDENTITY_OCR := tests/test_identity_ocr
|
||||||
TEST_IDENTITY_OCR_PREPROCESSOR := tests/test_identity_ocr_preprocessor
|
TEST_IDENTITY_OCR_PREPROCESSOR := tests/test_identity_ocr_preprocessor
|
||||||
|
TEST_IDENTITY_TRACEABILITY := tests/test_identity_traceability
|
||||||
TEST_OCR_PROVENANCE_OVERLAY_GTK := tests/test_ocr_provenance_overlay_gtk
|
TEST_OCR_PROVENANCE_OVERLAY_GTK := tests/test_ocr_provenance_overlay_gtk
|
||||||
TEST_EVIDENCE_METADATA_DIALOG_GTK := tests/test_evidence_metadata_dialog_gtk
|
TEST_EVIDENCE_METADATA_DIALOG_GTK := tests/test_evidence_metadata_dialog_gtk
|
||||||
TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk
|
TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk
|
||||||
|
|
@ -200,6 +231,17 @@ $(TEST_IDENTITY_OCR_PREPROCESSOR): tests/test_identity_ocr_preprocessor.c \
|
||||||
src/core/tool_registry.c
|
src/core/tool_registry.c
|
||||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
$(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/models/person_role_assignment.c \
|
||||||
|
src/models/identity_ocr.c src/dao/identity_ocr_dao.c \
|
||||||
|
src/core/relation_type_normalizer.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)
|
||||||
|
|
||||||
$(TEST_OCR_PROVENANCE_OVERLAY_GTK): \
|
$(TEST_OCR_PROVENANCE_OVERLAY_GTK): \
|
||||||
tests/test_ocr_provenance_overlay_gtk.c \
|
tests/test_ocr_provenance_overlay_gtk.c \
|
||||||
src/widgets/ocr_provenance_overlay.c src/core/ocr_region_geometry.c \
|
src/widgets/ocr_provenance_overlay.c src/core/ocr_region_geometry.c \
|
||||||
|
|
@ -890,6 +932,8 @@ $(TEST_PERSON_DIALOG_LIFECYCLE): tests/test_person_dialog_lifecycle.c \
|
||||||
|
|
||||||
$(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.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/create_person_dialog.c src/views/dialog_geometry.c \
|
||||||
|
src/views/person_vocabulary_adapter.c \
|
||||||
|
src/views/identity_ocr_option_adapter.c \
|
||||||
src/core/person_dialog_lifecycle.c \
|
src/core/person_dialog_lifecycle.c \
|
||||||
src/core/person_confirmation_summary.c \
|
src/core/person_confirmation_summary.c \
|
||||||
src/core/evidence_staging.c src/core/evidence_staging_task.c \
|
src/core/evidence_staging.c src/core/evidence_staging_task.c \
|
||||||
|
|
@ -908,12 +952,18 @@ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
|
||||||
src/core/background_task.c src/core/task_manager.c \
|
src/core/background_task.c src/core/task_manager.c \
|
||||||
src/models/evidence_selection_model.c src/models/evidence_record.c \
|
src/models/evidence_selection_model.c src/models/evidence_record.c \
|
||||||
src/models/person_role_assignment.c \
|
src/models/person_role_assignment.c \
|
||||||
src/models/person_evidence_selection.c
|
src/models/person_evidence_selection.c \
|
||||||
|
src/models/identity_traceability.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)
|
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
$(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
|
$(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
|
||||||
tests/test_create_person_dialog_ocr_gtk.c \
|
tests/test_create_person_dialog_ocr_gtk.c \
|
||||||
src/views/create_person_dialog.c src/views/dialog_geometry.c \
|
src/views/create_person_dialog.c src/views/dialog_geometry.c \
|
||||||
|
src/views/person_vocabulary_adapter.c \
|
||||||
|
src/views/identity_ocr_option_adapter.c \
|
||||||
src/core/person_dialog_lifecycle.c \
|
src/core/person_dialog_lifecycle.c \
|
||||||
src/core/person_confirmation_summary.c \
|
src/core/person_confirmation_summary.c \
|
||||||
src/core/evidence_staging.c src/core/evidence_staging_task.c \
|
src/core/evidence_staging.c src/core/evidence_staging_task.c \
|
||||||
|
|
@ -937,6 +987,7 @@ $(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
|
||||||
src/models/evidence_selection_model.c src/models/evidence_record.c \
|
src/models/evidence_selection_model.c src/models/evidence_record.c \
|
||||||
src/models/person_role_assignment.c \
|
src/models/person_role_assignment.c \
|
||||||
src/models/person_evidence_selection.c \
|
src/models/person_evidence_selection.c \
|
||||||
|
src/models/identity_traceability.c src/dao/identity_traceability_dao.c \
|
||||||
src/database/database.c src/database/schema.c src/database/statement.c \
|
src/database/database.c src/database/schema.c src/database/statement.c \
|
||||||
src/database/transaction.c src/database/error.c \
|
src/database/transaction.c src/database/error.c \
|
||||||
src/core/relation_type_normalizer.c $(FAKE_DOCUMENT_TOOL)
|
src/core/relation_type_normalizer.c $(FAKE_DOCUMENT_TOOL)
|
||||||
|
|
@ -981,6 +1032,7 @@ $(TEST_PERSON_CREATION_COORDINATOR): \
|
||||||
src/core/person_creation_coordinator.c src/core/evidence_staging.c \
|
src/core/person_creation_coordinator.c src/core/evidence_staging.c \
|
||||||
src/core/file_hash.c \
|
src/core/file_hash.c \
|
||||||
src/models/identity_ocr.c src/dao/identity_ocr_dao.c \
|
src/models/identity_ocr.c src/dao/identity_ocr_dao.c \
|
||||||
|
src/models/identity_traceability.c src/dao/identity_traceability_dao.c \
|
||||||
src/models/person_evidence_selection.c src/models/evidence_record.c \
|
src/models/person_evidence_selection.c src/models/evidence_record.c \
|
||||||
src/models/person_role_assignment.c src/models/entity_record.c \
|
src/models/person_role_assignment.c src/models/entity_record.c \
|
||||||
src/models/evidence_observation.c \
|
src/models/evidence_observation.c \
|
||||||
|
|
@ -1127,6 +1179,7 @@ test: \
|
||||||
$(TEST_DOCUMENT_TOOL_RUNNER) \
|
$(TEST_DOCUMENT_TOOL_RUNNER) \
|
||||||
$(TEST_IDENTITY_OCR) \
|
$(TEST_IDENTITY_OCR) \
|
||||||
$(TEST_IDENTITY_OCR_PREPROCESSOR) \
|
$(TEST_IDENTITY_OCR_PREPROCESSOR) \
|
||||||
|
$(TEST_IDENTITY_TRACEABILITY) \
|
||||||
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
|
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
|
||||||
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
||||||
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
|
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
|
||||||
|
|
@ -1221,6 +1274,7 @@ test: \
|
||||||
@$(TEST_DOCUMENT_TOOL_RUNNER)
|
@$(TEST_DOCUMENT_TOOL_RUNNER)
|
||||||
@$(TEST_IDENTITY_OCR)
|
@$(TEST_IDENTITY_OCR)
|
||||||
@$(TEST_IDENTITY_OCR_PREPROCESSOR)
|
@$(TEST_IDENTITY_OCR_PREPROCESSOR)
|
||||||
|
@$(TEST_IDENTITY_TRACEABILITY)
|
||||||
@$(TEST_OCR_PROVENANCE_OVERLAY_GTK)
|
@$(TEST_OCR_PROVENANCE_OVERLAY_GTK)
|
||||||
@$(TEST_EVIDENCE_METADATA_DIALOG_GTK)
|
@$(TEST_EVIDENCE_METADATA_DIALOG_GTK)
|
||||||
@$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK)
|
@$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK)
|
||||||
|
|
@ -1319,6 +1373,7 @@ clean:
|
||||||
$(TEST_DOCUMENT_TOOL_RUNNER) \
|
$(TEST_DOCUMENT_TOOL_RUNNER) \
|
||||||
$(TEST_IDENTITY_OCR) \
|
$(TEST_IDENTITY_OCR) \
|
||||||
$(TEST_IDENTITY_OCR_PREPROCESSOR) \
|
$(TEST_IDENTITY_OCR_PREPROCESSOR) \
|
||||||
|
$(TEST_IDENTITY_TRACEABILITY) \
|
||||||
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
|
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
|
||||||
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
|
||||||
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
|
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,11 @@ CREATE TABLE IF NOT EXISTS identity_field_observations (
|
||||||
source_width INTEGER, source_height INTEGER, source_image_width INTEGER,
|
source_width INTEGER, source_height INTEGER, source_image_width INTEGER,
|
||||||
source_image_height INTEGER, display_order INTEGER NOT NULL,
|
source_image_height INTEGER, display_order INTEGER NOT NULL,
|
||||||
reviewed_at TEXT NOT NULL, review_note TEXT,
|
reviewed_at TEXT NOT NULL, review_note TEXT,
|
||||||
|
confirmed_value TEXT,
|
||||||
|
confirmation_state TEXT NOT NULL DEFAULT 'unconfirmed'
|
||||||
|
CHECK(confirmation_state IN ('unconfirmed','human_confirmed')),
|
||||||
|
value_quality TEXT NOT NULL DEFAULT 'complete'
|
||||||
|
CHECK(value_quality IN ('complete','partial','uncertain','invalid')),
|
||||||
CHECK (
|
CHECK (
|
||||||
(origin = 'manual_entry' AND raw_value IS NULL
|
(origin = 'manual_entry' AND raw_value IS NULL
|
||||||
AND corrected_value IS NOT NULL AND confidence IS NULL)
|
AND corrected_value IS NOT NULL AND confidence IS NULL)
|
||||||
|
|
@ -318,3 +323,116 @@ CREATE TABLE IF NOT EXISTS identity_field_observations (
|
||||||
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE);
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE);
|
||||||
CREATE INDEX IF NOT EXISTS idx_identity_fields_observation
|
CREATE INDEX IF NOT EXISTS idx_identity_fields_observation
|
||||||
ON identity_field_observations(observation_id,display_order);
|
ON identity_field_observations(observation_id,display_order);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS identification_status_vocabulary(
|
||||||
|
code TEXT PRIMARY KEY,label TEXT NOT NULL,description TEXT NOT NULL,
|
||||||
|
display_order INTEGER NOT NULL UNIQUE,
|
||||||
|
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN(0,1)),
|
||||||
|
requires_justification INTEGER NOT NULL DEFAULT 0 CHECK(requires_justification IN(0,1)),
|
||||||
|
sensitive INTEGER NOT NULL DEFAULT 0 CHECK(sensitive IN(0,1)));
|
||||||
|
INSERT OR IGNORE INTO identification_status_vocabulary VALUES
|
||||||
|
('unknown','Inconnu','Aucune identification disponible.',10,1,0,0),
|
||||||
|
('unverified','Non vérifié','Identification déclarée, non vérifiée.',20,1,0,0),
|
||||||
|
('presumed','Présumé','Identification présumée à justifier.',30,1,1,1),
|
||||||
|
('partially_identified','Partiellement identifié','Identification incomplète.',40,1,0,0),
|
||||||
|
('confirmed','Confirmé','Identification confirmée à justifier.',50,1,1,1),
|
||||||
|
('disputed','Contesté','Identification contestée à justifier.',60,1,1,1);
|
||||||
|
CREATE TABLE IF NOT EXISTS person_role_vocabulary(
|
||||||
|
code TEXT PRIMARY KEY,label TEXT NOT NULL,description TEXT NOT NULL,
|
||||||
|
display_order INTEGER NOT NULL UNIQUE,active INTEGER NOT NULL CHECK(active IN(0,1)),
|
||||||
|
requires_justification INTEGER NOT NULL CHECK(requires_justification IN(0,1)),
|
||||||
|
sensitive INTEGER NOT NULL CHECK(sensitive IN(0,1)));
|
||||||
|
INSERT OR IGNORE INTO person_role_vocabulary VALUES
|
||||||
|
('alleged_author','Auteur présumé','Rôle allégué.',10,1,1,1),
|
||||||
|
('presented_identity','Identité présentée','Identité déclarée.',20,1,0,0),
|
||||||
|
('potentially_impersonated_identity','Identité potentiellement usurpée','Hypothèse sensible.',30,1,1,1),
|
||||||
|
('victim','Victime','Personne déclarée victime.',40,1,0,0),
|
||||||
|
('witness','Témoin','Personne déclarée témoin.',50,1,0,0),
|
||||||
|
('declared_bank_holder','Titulaire bancaire déclaré','Titulaire déclaré.',60,1,0,0),
|
||||||
|
('intermediary','Intermédiaire','Intermédiaire observé.',70,1,0,0),
|
||||||
|
('mentioned_person','Personne citée','Personne citée.',80,1,0,0),
|
||||||
|
('other','Autre','Rôle factuel non couvert.',90,1,1,0),
|
||||||
|
('uncategorized','Non catégorisée','Code historique.',100,1,0,0),
|
||||||
|
('alleged_scammer','Escroc présumé (historique)','Code historique.',110,0,1,1),
|
||||||
|
('suspect','Suspect (historique)','Code historique.',120,0,1,1),
|
||||||
|
('related_person','Personne liée (historique)','Code historique.',130,0,0,0),
|
||||||
|
('impersonated_identity','Identité usurpée (historique)','Code historique.',140,0,1,1);
|
||||||
|
CREATE TABLE IF NOT EXISTS document_authenticity_assessments(
|
||||||
|
id TEXT PRIMARY KEY,evidence_id TEXT NOT NULL,ocr_run_id TEXT,status TEXT NOT NULL
|
||||||
|
CHECK(status IN('indeterminate','presumed_authentic','suspicious',
|
||||||
|
'presumed_forged','confirmed_forged')),justification TEXT,
|
||||||
|
assessed_at TEXT NOT NULL,previous_assessment_id TEXT,technical_note TEXT,
|
||||||
|
origin TEXT NOT NULL CHECK(origin='human'),
|
||||||
|
CHECK(status='indeterminate' OR
|
||||||
|
(justification IS NOT NULL AND length(trim(justification))>0)),
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY(previous_assessment_id) REFERENCES document_authenticity_assessments(id));
|
||||||
|
CREATE TABLE IF NOT EXISTS person_evidence_factual_relations(
|
||||||
|
id TEXT PRIMARY KEY,person_id TEXT NOT NULL,evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT,relation_type TEXT NOT NULL CHECK(relation_type IN(
|
||||||
|
'identity_observed_in','document_presented_in_name_of','declared_holder_in',
|
||||||
|
'data_extracted_from')),factual_note TEXT,observed_at TEXT NOT NULL,
|
||||||
|
origin TEXT NOT NULL CHECK(origin='human'),active INTEGER NOT NULL DEFAULT 1
|
||||||
|
CHECK(active IN(0,1)),FOREIGN KEY(person_id) REFERENCES entites(id),
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id),
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE SET NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS person_identification_assessments(
|
||||||
|
id TEXT PRIMARY KEY,person_id TEXT NOT NULL,status_code TEXT NOT NULL,
|
||||||
|
justification TEXT,assessed_at TEXT NOT NULL,origin TEXT NOT NULL CHECK(origin='human'),
|
||||||
|
previous_assessment_id TEXT,CHECK(status_code<>'disputed' OR
|
||||||
|
(justification IS NOT NULL AND length(trim(justification))>0)),
|
||||||
|
FOREIGN KEY(person_id) REFERENCES entites(id),
|
||||||
|
FOREIGN KEY(status_code) REFERENCES identification_status_vocabulary(code),
|
||||||
|
FOREIGN KEY(previous_assessment_id) REFERENCES person_identification_assessments(id));
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_authenticity_evidence_history
|
||||||
|
ON document_authenticity_assessments(evidence_id,assessed_at,id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_person_evidence_factual_person
|
||||||
|
ON person_evidence_factual_relations(person_id,observed_at,id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_person_evidence_factual_evidence
|
||||||
|
ON person_evidence_factual_relations(evidence_id,observed_at,id);
|
||||||
|
CREATE TRIGGER IF NOT EXISTS authenticity_v18_consistency
|
||||||
|
BEFORE INSERT ON document_authenticity_assessments BEGIN
|
||||||
|
SELECT CASE WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(
|
||||||
|
SELECT 1 FROM identity_ocr_runs WHERE id=NEW.ocr_run_id
|
||||||
|
AND evidence_id=NEW.evidence_id)
|
||||||
|
THEN RAISE(ABORT,'OCR run belongs to another evidence')
|
||||||
|
WHEN NEW.previous_assessment_id IS NOT NULL AND NOT EXISTS(
|
||||||
|
SELECT 1 FROM document_authenticity_assessments
|
||||||
|
WHERE id=NEW.previous_assessment_id AND evidence_id=NEW.evidence_id)
|
||||||
|
THEN RAISE(ABORT,'previous assessment belongs to another evidence') END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER IF NOT EXISTS factual_relation_v18_consistency
|
||||||
|
BEFORE INSERT ON person_evidence_factual_relations BEGIN
|
||||||
|
SELECT CASE WHEN NOT EXISTS(SELECT 1 FROM entites e JOIN types_entite t
|
||||||
|
ON t.id=e.type_id WHERE e.id=NEW.person_id AND t.code='person')
|
||||||
|
THEN RAISE(ABORT,'factual relation requires a person')
|
||||||
|
WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM identity_ocr_runs
|
||||||
|
WHERE id=NEW.ocr_run_id AND evidence_id=NEW.evidence_id)
|
||||||
|
THEN RAISE(ABORT,'OCR run belongs to another evidence') END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER IF NOT EXISTS identity_fields_v18_insert_guard
|
||||||
|
BEFORE INSERT ON identity_field_observations BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN NEW.review_status IN('rejected','conflict') AND NEW.confirmed_value IS NOT NULL
|
||||||
|
THEN RAISE(ABORT,'non-projectable review status')
|
||||||
|
WHEN NEW.value_quality IN('uncertain','invalid') AND NEW.confirmed_value IS NOT NULL
|
||||||
|
THEN RAISE(ABORT,'non-projectable quality')
|
||||||
|
WHEN NEW.confirmed_value IS NOT NULL AND NEW.confirmation_state<>'human_confirmed'
|
||||||
|
THEN RAISE(ABORT,'human confirmation required')
|
||||||
|
WHEN NEW.confirmation_state='human_confirmed' AND NEW.confirmed_value IS NULL
|
||||||
|
THEN RAISE(ABORT,'confirmed value required') END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER IF NOT EXISTS identity_fields_v18_update_guard
|
||||||
|
BEFORE UPDATE ON identity_field_observations BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN NEW.review_status IN('rejected','conflict') AND NEW.confirmed_value IS NOT NULL
|
||||||
|
THEN RAISE(ABORT,'non-projectable review status')
|
||||||
|
WHEN NEW.value_quality IN('uncertain','invalid') AND NEW.confirmed_value IS NOT NULL
|
||||||
|
THEN RAISE(ABORT,'non-projectable quality')
|
||||||
|
WHEN NEW.confirmed_value IS NOT NULL AND NEW.confirmation_state<>'human_confirmed'
|
||||||
|
THEN RAISE(ABORT,'human confirmation required')
|
||||||
|
WHEN NEW.confirmation_state='human_confirmed' AND NEW.confirmed_value IS NULL
|
||||||
|
THEN RAISE(ABORT,'confirmed value required') END;
|
||||||
|
END;
|
||||||
|
|
|
||||||
217
database/schema_v18.sql
Normal file
217
database/schema_v18.sql
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
/* Migration V18 — traçabilité humaine des documents d'identité. */
|
||||||
|
|
||||||
|
CREATE TABLE identification_status_vocabulary (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
display_order INTEGER NOT NULL UNIQUE,
|
||||||
|
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
|
||||||
|
requires_justification INTEGER NOT NULL DEFAULT 0
|
||||||
|
CHECK(requires_justification IN (0,1)),
|
||||||
|
sensitive INTEGER NOT NULL DEFAULT 0 CHECK(sensitive IN (0,1))
|
||||||
|
);
|
||||||
|
INSERT INTO identification_status_vocabulary VALUES
|
||||||
|
('unknown','Inconnu','Aucune identification disponible.',10,1,0,0),
|
||||||
|
('unverified','Non vérifié','Identification déclarée, non vérifiée.',20,1,0,0),
|
||||||
|
('presumed','Présumé','Identification présumée à justifier.',30,1,1,1),
|
||||||
|
('partially_identified','Partiellement identifié','Identification incomplète.',40,1,0,0),
|
||||||
|
('confirmed','Confirmé','Identification confirmée à justifier.',50,1,1,1),
|
||||||
|
('disputed','Contesté','Identification contestée à justifier.',60,1,1,1);
|
||||||
|
|
||||||
|
CREATE TABLE person_role_vocabulary (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
display_order INTEGER NOT NULL UNIQUE,
|
||||||
|
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
|
||||||
|
requires_justification INTEGER NOT NULL DEFAULT 0
|
||||||
|
CHECK(requires_justification IN (0,1)),
|
||||||
|
sensitive INTEGER NOT NULL DEFAULT 0 CHECK(sensitive IN (0,1))
|
||||||
|
);
|
||||||
|
INSERT INTO person_role_vocabulary VALUES
|
||||||
|
('alleged_author','Auteur présumé','Rôle allégué, sans attribution automatique.',10,1,1,1),
|
||||||
|
('presented_identity','Identité présentée','Identité déclarée ou présentée dans une preuve.',20,1,0,0),
|
||||||
|
('potentially_impersonated_identity','Identité potentiellement usurpée','Hypothèse sensible nécessitant une justification.',30,1,1,1),
|
||||||
|
('victim','Victime','Personne déclarée victime.',40,1,0,0),
|
||||||
|
('witness','Témoin','Personne déclarée témoin.',50,1,0,0),
|
||||||
|
('declared_bank_holder','Titulaire bancaire déclaré','Titulaire déclaré par une source.',60,1,0,0),
|
||||||
|
('intermediary','Intermédiaire','Personne observée comme intermédiaire.',70,1,0,0),
|
||||||
|
('mentioned_person','Personne citée','Personne seulement citée.',80,1,0,0),
|
||||||
|
('other','Autre','Rôle factuel non couvert.',90,1,1,0),
|
||||||
|
('uncategorized','Non catégorisée','Code historique.',100,1,0,0),
|
||||||
|
('alleged_scammer','Escroc présumé (historique)','Code historique sensible.',110,0,1,1),
|
||||||
|
('suspect','Suspect (historique)','Code historique sensible.',120,0,1,1),
|
||||||
|
('related_person','Personne liée (historique)','Code historique.',130,0,0,0),
|
||||||
|
('impersonated_identity','Identité usurpée (historique)','Code historique sensible.',140,0,1,1);
|
||||||
|
|
||||||
|
CREATE TABLE document_authenticity_assessments (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT,
|
||||||
|
status TEXT NOT NULL CHECK(status IN (
|
||||||
|
'indeterminate','presumed_authentic','suspicious',
|
||||||
|
'presumed_forged','confirmed_forged')),
|
||||||
|
justification TEXT,
|
||||||
|
assessed_at TEXT NOT NULL CHECK(length(assessed_at)=20),
|
||||||
|
previous_assessment_id TEXT,
|
||||||
|
technical_note TEXT,
|
||||||
|
origin TEXT NOT NULL CHECK(origin='human'),
|
||||||
|
CHECK(status='indeterminate' OR
|
||||||
|
(justification IS NOT NULL AND length(trim(justification))>0)),
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY(previous_assessment_id)
|
||||||
|
REFERENCES document_authenticity_assessments(id) ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_authenticity_evidence_history
|
||||||
|
ON document_authenticity_assessments(evidence_id,assessed_at,id);
|
||||||
|
CREATE UNIQUE INDEX idx_authenticity_previous
|
||||||
|
ON document_authenticity_assessments(previous_assessment_id)
|
||||||
|
WHERE previous_assessment_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE TABLE person_evidence_factual_relations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
person_id TEXT NOT NULL,
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT,
|
||||||
|
relation_type TEXT NOT NULL CHECK(relation_type IN (
|
||||||
|
'identity_observed_in','document_presented_in_name_of',
|
||||||
|
'declared_holder_in','data_extracted_from')),
|
||||||
|
factual_note TEXT,
|
||||||
|
observed_at TEXT NOT NULL CHECK(length(observed_at)=20),
|
||||||
|
origin TEXT NOT NULL CHECK(origin='human'),
|
||||||
|
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
|
||||||
|
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_person_evidence_factual_person
|
||||||
|
ON person_evidence_factual_relations(person_id,observed_at,id);
|
||||||
|
CREATE INDEX idx_person_evidence_factual_evidence
|
||||||
|
ON person_evidence_factual_relations(evidence_id,observed_at,id);
|
||||||
|
CREATE TRIGGER authenticity_v18_consistency
|
||||||
|
BEFORE INSERT ON document_authenticity_assessments BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(
|
||||||
|
SELECT 1 FROM identity_ocr_runs
|
||||||
|
WHERE id=NEW.ocr_run_id AND evidence_id=NEW.evidence_id)
|
||||||
|
THEN RAISE(ABORT,'OCR run belongs to another evidence')
|
||||||
|
WHEN NEW.previous_assessment_id IS NOT NULL AND NOT EXISTS(
|
||||||
|
SELECT 1 FROM document_authenticity_assessments
|
||||||
|
WHERE id=NEW.previous_assessment_id AND evidence_id=NEW.evidence_id)
|
||||||
|
THEN RAISE(ABORT,'previous assessment belongs to another evidence') END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER factual_relation_v18_consistency
|
||||||
|
BEFORE INSERT ON person_evidence_factual_relations BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN NOT EXISTS(SELECT 1 FROM entites e JOIN types_entite t ON t.id=e.type_id
|
||||||
|
WHERE e.id=NEW.person_id AND t.code='person')
|
||||||
|
THEN RAISE(ABORT,'factual relation requires a person')
|
||||||
|
WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(
|
||||||
|
SELECT 1 FROM identity_ocr_runs
|
||||||
|
WHERE id=NEW.ocr_run_id AND evidence_id=NEW.evidence_id)
|
||||||
|
THEN RAISE(ABORT,'OCR run belongs to another evidence') END;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TABLE person_identification_assessments (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
person_id TEXT NOT NULL,
|
||||||
|
status_code TEXT NOT NULL,
|
||||||
|
justification TEXT,
|
||||||
|
assessed_at TEXT NOT NULL CHECK(length(assessed_at)=20),
|
||||||
|
origin TEXT NOT NULL CHECK(origin='human'),
|
||||||
|
previous_assessment_id TEXT,
|
||||||
|
CHECK(status_code<>'disputed' OR
|
||||||
|
(justification IS NOT NULL AND length(trim(justification))>0)),
|
||||||
|
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(status_code) REFERENCES identification_status_vocabulary(code)
|
||||||
|
ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY(previous_assessment_id)
|
||||||
|
REFERENCES person_identification_assessments(id) ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE identity_field_observations_v18 (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
observation_id TEXT NOT NULL,
|
||||||
|
field_code TEXT NOT NULL,
|
||||||
|
raw_value TEXT,
|
||||||
|
corrected_value TEXT,
|
||||||
|
normalized_value TEXT,
|
||||||
|
confidence REAL CHECK(confidence IS NULL OR confidence BETWEEN 0 AND 100),
|
||||||
|
review_status TEXT NOT NULL CHECK(review_status IN
|
||||||
|
('proposed','accepted','modified','rejected','conflict')),
|
||||||
|
origin TEXT NOT NULL CHECK(origin IN
|
||||||
|
('ocr','mrz','manual_override','manual_entry')),
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
ocr_run_id TEXT NOT NULL,
|
||||||
|
page_number INTEGER NOT NULL CHECK(page_number>0),
|
||||||
|
source_x INTEGER,source_y INTEGER,source_width INTEGER,source_height INTEGER,
|
||||||
|
source_image_width INTEGER,source_image_height INTEGER,
|
||||||
|
display_order INTEGER NOT NULL CHECK(display_order>=0),
|
||||||
|
reviewed_at TEXT NOT NULL CHECK(length(reviewed_at)=20),
|
||||||
|
review_note TEXT,
|
||||||
|
confirmed_value TEXT,
|
||||||
|
confirmation_state TEXT NOT NULL DEFAULT 'unconfirmed'
|
||||||
|
CHECK(confirmation_state IN ('unconfirmed','human_confirmed')),
|
||||||
|
value_quality TEXT NOT NULL DEFAULT 'complete'
|
||||||
|
CHECK(value_quality IN ('complete','partial','uncertain','invalid')),
|
||||||
|
CHECK((origin='manual_entry' AND raw_value IS NULL
|
||||||
|
AND corrected_value IS NOT NULL AND confidence IS NULL)
|
||||||
|
OR (origin<>'manual_entry' AND raw_value IS NOT NULL)),
|
||||||
|
FOREIGN KEY(observation_id) REFERENCES identity_document_observations(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO identity_field_observations_v18(
|
||||||
|
id,observation_id,field_code,raw_value,corrected_value,normalized_value,
|
||||||
|
confidence,review_status,origin,evidence_id,ocr_run_id,page_number,
|
||||||
|
source_x,source_y,source_width,source_height,source_image_width,
|
||||||
|
source_image_height,display_order,reviewed_at,review_note)
|
||||||
|
SELECT id,observation_id,field_code,raw_value,corrected_value,normalized_value,
|
||||||
|
confidence,review_status,origin,evidence_id,ocr_run_id,page_number,
|
||||||
|
source_x,source_y,source_width,source_height,source_image_width,
|
||||||
|
source_image_height,display_order,reviewed_at,review_note
|
||||||
|
FROM identity_field_observations;
|
||||||
|
DROP TABLE identity_field_observations;
|
||||||
|
ALTER TABLE identity_field_observations_v18
|
||||||
|
RENAME TO identity_field_observations;
|
||||||
|
CREATE INDEX idx_identity_fields_observation
|
||||||
|
ON identity_field_observations(observation_id,display_order);
|
||||||
|
|
||||||
|
CREATE TRIGGER identity_fields_v18_insert_guard
|
||||||
|
BEFORE INSERT ON identity_field_observations
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN NEW.review_status IN ('rejected','conflict')
|
||||||
|
AND NEW.confirmed_value IS NOT NULL
|
||||||
|
THEN RAISE(ABORT,'rejected or conflict field cannot be confirmed')
|
||||||
|
WHEN NEW.value_quality IN ('uncertain','invalid')
|
||||||
|
AND NEW.confirmed_value IS NOT NULL
|
||||||
|
THEN RAISE(ABORT,'uncertain or invalid field cannot be confirmed')
|
||||||
|
WHEN NEW.confirmed_value IS NOT NULL
|
||||||
|
AND NEW.confirmation_state<>'human_confirmed'
|
||||||
|
THEN RAISE(ABORT,'confirmed value requires human confirmation')
|
||||||
|
WHEN NEW.confirmation_state='human_confirmed'
|
||||||
|
AND NEW.confirmed_value IS NULL
|
||||||
|
THEN RAISE(ABORT,'human confirmation requires a value')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER identity_fields_v18_update_guard
|
||||||
|
BEFORE UPDATE ON identity_field_observations
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN NEW.review_status IN ('rejected','conflict')
|
||||||
|
AND NEW.confirmed_value IS NOT NULL
|
||||||
|
THEN RAISE(ABORT,'rejected or conflict field cannot be confirmed')
|
||||||
|
WHEN NEW.value_quality IN ('uncertain','invalid')
|
||||||
|
AND NEW.confirmed_value IS NOT NULL
|
||||||
|
THEN RAISE(ABORT,'uncertain or invalid field cannot be confirmed')
|
||||||
|
WHEN NEW.confirmed_value IS NOT NULL
|
||||||
|
AND NEW.confirmation_state<>'human_confirmed'
|
||||||
|
THEN RAISE(ABORT,'confirmed value requires human confirmation')
|
||||||
|
WHEN NEW.confirmation_state='human_confirmed'
|
||||||
|
AND NEW.confirmed_value IS NULL
|
||||||
|
THEN RAISE(ABORT,'human confirmation requires a value')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
|
@ -30,7 +30,7 @@ pause, retour au début, détachement et libération via des actions injectées.
|
||||||
|
|
||||||
> **Version :** 3.2
|
> **Version :** 3.2
|
||||||
> **Dernière mise à jour :** 2026-07-30
|
> **Dernière mise à jour :** 2026-07-30
|
||||||
> **Schéma SQLite courant :** V17
|
> **Schéma SQLite courant :** V18
|
||||||
|
|
||||||
## Personnes contextuelles — SQLite V14
|
## Personnes contextuelles — SQLite V14
|
||||||
|
|
||||||
|
|
@ -99,6 +99,31 @@ première allocation réelle puis reste entièrement modifiable. Les actions
|
||||||
restent fixes en bas. Cette politique exclut les alertes simples, les popups
|
restent fixes en bas. Cette politique exclut les alertes simples, les popups
|
||||||
`GtkDropDown` et les sélecteurs de fichiers natifs.
|
`GtkDropDown` et les sélecteurs de fichiers natifs.
|
||||||
|
|
||||||
|
## Traçabilité d’identité — SQLite V18
|
||||||
|
|
||||||
|
V18 sépare quatre dimensions qui ne doivent jamais être confondues :
|
||||||
|
l’identification d’une personne, son rôle contextuel, l’authenticité humaine
|
||||||
|
d’un document et la qualité d’une valeur OCR. Les statuts d’identification et
|
||||||
|
les rôles proviennent de vocabulaires SQLite ordonnés. Les codes historiques
|
||||||
|
de rôles sont conservés, mais les rôles sensibles ne sont jamais assignés par
|
||||||
|
un moteur automatique.
|
||||||
|
|
||||||
|
`document_authenticity_assessments` conserve un historique append-only
|
||||||
|
d’évaluations humaines. Toute conclusion autre que `indeterminate` exige une
|
||||||
|
justification. `person_evidence_factual_relations` porte uniquement quatre
|
||||||
|
relations factuelles contrôlées et humaines ; elle ne remplace ni
|
||||||
|
`preuve_entites`, rattachement générique, ni `relations`, relation entre
|
||||||
|
entités. Aucun type « auteur », « identité réelle », « a falsifié » ou
|
||||||
|
« a usurpé » n’est accepté.
|
||||||
|
|
||||||
|
Les observations OCR conservent désormais séparément `raw_value`,
|
||||||
|
`normalized_value`, `corrected_value` et `confirmed_value`. La confirmation
|
||||||
|
humaine et la qualification `complete`, `partial`, `uncertain` ou `invalid`
|
||||||
|
sont orthogonales au statut de révision. Les contraintes SQLite et le service
|
||||||
|
de validation interdisent la projection des valeurs rejetées, en conflit,
|
||||||
|
incertaines ou invalides. Cette tranche ne réalise encore aucune projection
|
||||||
|
automatique et ne fournit pas l’interface GTK complète de saisie.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Objectif
|
## 1. Objectif
|
||||||
|
|
@ -395,7 +420,7 @@ validation du chemin
|
||||||
↓
|
↓
|
||||||
création de l'arborescence
|
création de l'arborescence
|
||||||
↓
|
↓
|
||||||
initialisation transactionnelle de SQLite V17
|
initialisation transactionnelle de SQLite V18
|
||||||
↓
|
↓
|
||||||
création de l'identité de l'enquête
|
création de l'identité de l'enquête
|
||||||
↓
|
↓
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ achèvement du ticket #109.
|
||||||
|
|
||||||
> **Dernière mise à jour :** 2026-07-30
|
> **Dernière mise à jour :** 2026-07-30
|
||||||
> **État du projet :** développement actif
|
> **État du projet :** développement actif
|
||||||
> **Schéma SQLite courant :** V17
|
> **Schéma SQLite courant :** V18
|
||||||
> **Usage opérationnel :** non prêt pour la production
|
> **Usage opérationnel :** non prêt pour la production
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -131,7 +131,7 @@ La branche `main` contient notamment :
|
||||||
|
|
||||||
- création, validation et ouverture d'enquêtes ;
|
- création, validation et ouverture d'enquêtes ;
|
||||||
- session d'enquête remplaçable proprement ;
|
- session d'enquête remplaçable proprement ;
|
||||||
- infrastructure SQLite et migrations jusqu'à V17 ;
|
- infrastructure SQLite et migrations jusqu'à V18 ;
|
||||||
- couche Database, DAO et services métier ;
|
- couche Database, DAO et services métier ;
|
||||||
- import de preuves avec copie contrôlée et SHA-256 ;
|
- import de preuves avec copie contrôlée et SHA-256 ;
|
||||||
- vérification d'intégrité et reclassement des preuves ;
|
- vérification d'intégrité et reclassement des preuves ;
|
||||||
|
|
@ -175,21 +175,19 @@ PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE
|
||||||
- consultation, révision sans Tesseract et nouvelle analyse depuis la fiche ;
|
- consultation, révision sans Tesseract et nouvelle analyse depuis la fiche ;
|
||||||
- provenance graphique et aperçu partagé avec PDF multipage ;
|
- provenance graphique et aperçu partagé avec PDF multipage ;
|
||||||
- politique commune de géométrie des dialogues GTK métiers.
|
- politique commune de géométrie des dialogues GTK métiers.
|
||||||
|
- fondations V18 : authenticité humaine historisée, relations factuelles
|
||||||
|
typées, vocabulaires d’identification et de rôles, cycle OCR jusqu’à la
|
||||||
|
valeur explicitement confirmée et contraintes anti-automatisme.
|
||||||
|
|
||||||
Limitations restantes :
|
Limitations restantes :
|
||||||
|
|
||||||
1. l’OCR groupé de plusieurs preuves dans une même opération n’est pas pris en
|
1. l’OCR groupé de plusieurs preuves dans une même opération n’est pas pris en
|
||||||
charge ;
|
charge ;
|
||||||
2. le statut contrôlé d’authenticité du document et la justification
|
2. l’interface GTK complète de saisie de l’authenticité, des relations
|
||||||
obligatoire des statuts affirmatifs ne sont pas implémentés ;
|
factuelles, des états et des rôles V18 reste à construire ;
|
||||||
3. la relation factuelle typée entre personne et preuve et certains états
|
3. la provenance uniforme de tous les dérivés et la projection des seules
|
||||||
d’identification demandés restent incomplets ;
|
|
||||||
4. une partie du vocabulaire des rôles demeure codée en dur ;
|
|
||||||
5. la provenance uniforme de tous les dérivés et la projection des seules
|
|
||||||
valeurs OCR confirmées vers les attributs structurés de la personne restent
|
valeurs OCR confirmées vers les attributs structurés de la personne restent
|
||||||
partielles ;
|
partielles ;
|
||||||
6. les tests négatifs garantissant l’absence de relation automatique
|
|
||||||
attribuant l’identité ou le rôle d’auteur doivent encore être renforcés.
|
|
||||||
|
|
||||||
Ces limites interdisent de présenter le ticket #109 comme terminé.
|
Ces limites interdisent de présenter le ticket #109 comme terminé.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,21 @@
|
||||||
# Architecture de la base de données
|
# Architecture de la base de données
|
||||||
|
|
||||||
|
## Extension V18 — traçabilité d’identité
|
||||||
|
|
||||||
|
La migration transactionnelle `database/schema_v18.sql` ajoute les
|
||||||
|
vocabulaires `identification_status_vocabulary` et
|
||||||
|
`person_role_vocabulary`, l’historique
|
||||||
|
`document_authenticity_assessments`, les relations humaines contrôlées
|
||||||
|
`person_evidence_factual_relations` et l’historique
|
||||||
|
`person_identification_assessments`.
|
||||||
|
|
||||||
|
`identity_field_observations` distingue les valeurs brute, normalisée,
|
||||||
|
corrigée et confirmée. `confirmation_state` prouve l’action humaine et
|
||||||
|
`value_quality` sépare la complétude de la décision de révision. Les
|
||||||
|
contraintes refusent toute confirmation d’un champ rejeté, en conflit,
|
||||||
|
incertain ou invalide. Aucun OCR ne crée une évaluation d’authenticité, une
|
||||||
|
relation factuelle, un rôle sensible ou un état `confirmed`.
|
||||||
|
|
||||||
> **Statut :** architecture courante
|
> **Statut :** architecture courante
|
||||||
> **Version du schéma :** V10
|
> **Version du schéma :** V10
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-24
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "core/person_entity_service.h"
|
#include "core/person_entity_service.h"
|
||||||
#include "models/person_evidence_selection.h"
|
#include "models/person_evidence_selection.h"
|
||||||
#include "models/identity_ocr.h"
|
#include "models/identity_ocr.h"
|
||||||
|
#include "models/identity_traceability.h"
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
@ -31,12 +32,19 @@ typedef enum {
|
||||||
PERSON_CREATION_FAILURE_INSERT_DOCUMENT_OBSERVATION,
|
PERSON_CREATION_FAILURE_INSERT_DOCUMENT_OBSERVATION,
|
||||||
PERSON_CREATION_FAILURE_INSERT_FIELD,
|
PERSON_CREATION_FAILURE_INSERT_FIELD,
|
||||||
PERSON_CREATION_FAILURE_CREATE_SOURCE,
|
PERSON_CREATION_FAILURE_CREATE_SOURCE,
|
||||||
|
PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION,
|
||||||
PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,
|
PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,
|
||||||
PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,
|
PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,
|
||||||
PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,
|
PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,
|
||||||
PERSON_CREATION_FAILURE_COMMIT,
|
PERSON_CREATION_FAILURE_COMMIT,
|
||||||
PERSON_CREATION_FAILURE_COMPENSATION
|
PERSON_CREATION_FAILURE_COMPENSATION
|
||||||
} PersonCreationFailurePoint;
|
} PersonCreationFailurePoint;
|
||||||
|
typedef struct {
|
||||||
|
const char *evidence_selection_identifier;
|
||||||
|
const char *ocr_run_identifier;
|
||||||
|
const char *relation_type;
|
||||||
|
const char *factual_note;
|
||||||
|
} PersonCreationFactualRelationInput;
|
||||||
typedef gboolean (*PersonCreationSessionCheck)(gpointer user_data);
|
typedef gboolean (*PersonCreationSessionCheck)(gpointer user_data);
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PersonCreationFailurePoint failure_point;
|
PersonCreationFailurePoint failure_point;
|
||||||
|
|
@ -44,6 +52,8 @@ typedef struct {
|
||||||
gboolean inject_compensation_failure;
|
gboolean inject_compensation_failure;
|
||||||
PersonCreationSessionCheck session_check;
|
PersonCreationSessionCheck session_check;
|
||||||
gpointer session_check_data;
|
gpointer session_check_data;
|
||||||
|
/** PersonCreationFactualRelationInput* empruntés, choix humains explicites. */
|
||||||
|
const GPtrArray *factual_relations;
|
||||||
} PersonCreationCoordinatorOptions;
|
} PersonCreationCoordinatorOptions;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char *collected_at;
|
const char *collected_at;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ typedef struct {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char *id,*observation_id,*field_code,*raw_value,*corrected_value;
|
char *id,*observation_id,*field_code,*raw_value,*corrected_value;
|
||||||
char *normalized_value,*review_status,*origin,*evidence_id,*ocr_run_id;
|
char *normalized_value,*review_status,*origin,*evidence_id,*ocr_run_id;
|
||||||
|
char *confirmed_value,*confirmation_state,*value_quality;
|
||||||
char *reviewed_at,*review_note; double confidence; gboolean has_confidence;
|
char *reviewed_at,*review_note; double confidence; gboolean has_confidence;
|
||||||
gint64 page_number,source_x,source_y,source_width,source_height;
|
gint64 page_number,source_x,source_y,source_width,source_height;
|
||||||
gint64 source_image_width,source_image_height,display_order;
|
gint64 source_image_width,source_image_height,display_order;
|
||||||
|
|
@ -47,6 +48,8 @@ IdentityFieldObservationRecord *identity_ocr_dao_find_field(
|
||||||
IdentityOcrDao *dao,const char *identifier,GError **error);
|
IdentityOcrDao *dao,const char *identifier,GError **error);
|
||||||
GPtrArray *identity_ocr_dao_list_fields_by_document(IdentityOcrDao *dao,
|
GPtrArray *identity_ocr_dao_list_fields_by_document(IdentityOcrDao *dao,
|
||||||
const char *document_identifier,GError **error);
|
const char *document_identifier,GError **error);
|
||||||
|
GPtrArray *identity_ocr_dao_list_confirmed_fields(IdentityOcrDao *dao,
|
||||||
|
const char *document_identifier,GError **error);
|
||||||
IdentityOcrRun *identity_ocr_dao_load_run(
|
IdentityOcrRun *identity_ocr_dao_load_run(
|
||||||
IdentityOcrDao *dao,const char *investigation_root,
|
IdentityOcrDao *dao,const char *investigation_root,
|
||||||
const char *run_identifier,char **person_identifier,GError **error);
|
const char *run_identifier,char **person_identifier,GError **error);
|
||||||
|
|
|
||||||
29
include/dao/identity_traceability_dao.h
Normal file
29
include/dao/identity_traceability_dao.h
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#ifndef LABFY_IDENTITY_TRACEABILITY_DAO_H
|
||||||
|
#define LABFY_IDENTITY_TRACEABILITY_DAO_H
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "models/identity_traceability.h"
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef struct IdentityTraceabilityDao IdentityTraceabilityDao;
|
||||||
|
IdentityTraceabilityDao *identity_traceability_dao_new(Database *database);
|
||||||
|
void identity_traceability_dao_free(IdentityTraceabilityDao *dao);
|
||||||
|
gboolean identity_traceability_dao_insert_authenticity(
|
||||||
|
IdentityTraceabilityDao *dao,const DocumentAuthenticityAssessment *assessment,
|
||||||
|
GError **error);
|
||||||
|
DocumentAuthenticityAssessment *identity_traceability_dao_find_authenticity(
|
||||||
|
IdentityTraceabilityDao *dao,const char *identifier,GError **error);
|
||||||
|
GPtrArray *identity_traceability_dao_list_authenticity(
|
||||||
|
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
|
||||||
|
DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity(
|
||||||
|
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
|
||||||
|
gboolean identity_traceability_dao_insert_factual_relation(
|
||||||
|
IdentityTraceabilityDao *dao,const PersonEvidenceFactualRelation *relation,
|
||||||
|
GError **error);
|
||||||
|
GPtrArray *identity_traceability_dao_list_factual_relations(
|
||||||
|
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
|
||||||
|
GPtrArray *identity_traceability_dao_list_roles(
|
||||||
|
IdentityTraceabilityDao *dao,gboolean include_inactive,GError **error);
|
||||||
|
GPtrArray *identity_traceability_dao_list_identification_statuses(
|
||||||
|
IdentityTraceabilityDao *dao,gboolean include_inactive,GError **error);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -98,6 +98,7 @@ bool schema_install_v14(Database *database);
|
||||||
bool schema_install_v15(Database *database);
|
bool schema_install_v15(Database *database);
|
||||||
bool schema_install_v16(Database *database);
|
bool schema_install_v16(Database *database);
|
||||||
bool schema_install_v17(Database *database);
|
bool schema_install_v17(Database *database);
|
||||||
|
bool schema_install_v18(Database *database);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,22 @@ const char *identity_field_observation_get_raw_value(
|
||||||
const IdentityFieldObservation *field);
|
const IdentityFieldObservation *field);
|
||||||
const char *identity_field_observation_get_corrected_value(
|
const char *identity_field_observation_get_corrected_value(
|
||||||
const IdentityFieldObservation *field);
|
const IdentityFieldObservation *field);
|
||||||
|
const char *identity_field_observation_get_normalized_value(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
const char *identity_field_observation_get_confirmed_value(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
const char *identity_field_observation_get_value_quality(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
|
gboolean identity_field_observation_set_normalized_value(
|
||||||
|
IdentityFieldObservation *field, const char *value);
|
||||||
|
gboolean identity_field_observation_set_value_quality(
|
||||||
|
IdentityFieldObservation *field, const char *quality);
|
||||||
|
gboolean identity_field_observation_confirm(
|
||||||
|
IdentityFieldObservation *field, const char *value);
|
||||||
|
void identity_field_observation_clear_confirmation(
|
||||||
|
IdentityFieldObservation *field);
|
||||||
|
gboolean identity_field_observation_is_human_confirmed(
|
||||||
|
const IdentityFieldObservation *field);
|
||||||
IdentityReviewStatus identity_field_observation_get_status(
|
IdentityReviewStatus identity_field_observation_get_status(
|
||||||
const IdentityFieldObservation *field);
|
const IdentityFieldObservation *field);
|
||||||
double identity_field_observation_get_confidence(
|
double identity_field_observation_get_confidence(
|
||||||
|
|
|
||||||
58
include/models/identity_traceability.h
Normal file
58
include/models/identity_traceability.h
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
#ifndef LABFY_IDENTITY_TRACEABILITY_H
|
||||||
|
#define LABFY_IDENTITY_TRACEABILITY_H
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *identifier, *evidence_identifier, *ocr_run_identifier;
|
||||||
|
char *status, *justification, *assessed_at;
|
||||||
|
char *previous_identifier, *technical_note, *origin;
|
||||||
|
} DocumentAuthenticityAssessment;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *identifier, *person_identifier, *evidence_identifier;
|
||||||
|
char *ocr_run_identifier, *relation_type, *factual_note;
|
||||||
|
char *observed_at, *origin;
|
||||||
|
gboolean active;
|
||||||
|
} PersonEvidenceFactualRelation;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *code, *label, *description;
|
||||||
|
gint display_order;
|
||||||
|
gboolean active, requires_justification, sensitive;
|
||||||
|
} PersonRoleVocabularyEntry;
|
||||||
|
|
||||||
|
typedef PersonRoleVocabularyEntry IdentificationStatusVocabularyEntry;
|
||||||
|
|
||||||
|
gboolean identity_traceability_authenticity_status_valid(const char *status);
|
||||||
|
gboolean identity_traceability_relation_type_valid(const char *type);
|
||||||
|
gboolean identity_traceability_identification_status_valid(const char *status);
|
||||||
|
gboolean identity_traceability_value_quality_valid(const char *quality);
|
||||||
|
gboolean identity_traceability_field_is_projectable(
|
||||||
|
const char *review_status, const char *quality,
|
||||||
|
const char *confirmation_state, const char *confirmed_value);
|
||||||
|
|
||||||
|
DocumentAuthenticityAssessment *document_authenticity_assessment_new(
|
||||||
|
const char *identifier, const char *evidence_identifier,
|
||||||
|
const char *ocr_run_identifier, const char *status,
|
||||||
|
const char *justification, const char *assessed_at,
|
||||||
|
const char *previous_identifier, const char *technical_note);
|
||||||
|
DocumentAuthenticityAssessment *document_authenticity_assessment_copy(
|
||||||
|
const DocumentAuthenticityAssessment *assessment);
|
||||||
|
void document_authenticity_assessment_free(
|
||||||
|
DocumentAuthenticityAssessment *assessment);
|
||||||
|
|
||||||
|
PersonEvidenceFactualRelation *person_evidence_factual_relation_new(
|
||||||
|
const char *identifier, const char *person_identifier,
|
||||||
|
const char *evidence_identifier, const char *ocr_run_identifier,
|
||||||
|
const char *relation_type, const char *factual_note,
|
||||||
|
const char *observed_at, gboolean active);
|
||||||
|
PersonEvidenceFactualRelation *person_evidence_factual_relation_copy(
|
||||||
|
const PersonEvidenceFactualRelation *relation);
|
||||||
|
void person_evidence_factual_relation_free(
|
||||||
|
PersonEvidenceFactualRelation *relation);
|
||||||
|
void person_role_vocabulary_entry_free(PersonRoleVocabularyEntry *entry);
|
||||||
|
void identification_status_vocabulary_entry_free(
|
||||||
|
IdentificationStatusVocabularyEntry *entry);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -27,7 +27,8 @@ typedef gboolean (*CreatePersonDialogSessionCheck)(gpointer user_data);
|
||||||
* @return TRUE si le dialogue a été présenté.
|
* @return TRUE si le dialogue a été présenté.
|
||||||
*/
|
*/
|
||||||
gboolean create_person_dialog_present(GtkWindow *parent,
|
gboolean create_person_dialog_present(GtkWindow *parent,
|
||||||
const GPtrArray *evidence_records, const char *investigation_root_path,
|
Database *database, const GPtrArray *evidence_records,
|
||||||
|
const char *investigation_root_path,
|
||||||
TaskManager *task_manager, const ToolInfo *tesseract_tool,
|
TaskManager *task_manager, const ToolInfo *tesseract_tool,
|
||||||
CreatePersonDialogSessionCheck session_check,
|
CreatePersonDialogSessionCheck session_check,
|
||||||
CreatePersonDialogCallback callback, gpointer user_data,
|
CreatePersonDialogCallback callback, gpointer user_data,
|
||||||
|
|
|
||||||
9
include/views/identity_ocr_option_adapter.h
Normal file
9
include/views/identity_ocr_option_adapter.h
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#ifndef LABFY_IDENTITY_OCR_OPTION_ADAPTER_H
|
||||||
|
#define LABFY_IDENTITY_OCR_OPTION_ADAPTER_H
|
||||||
|
#include <glib.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
const char *identity_ocr_option_adapter_language_label(const char *code);
|
||||||
|
guint identity_ocr_option_adapter_default_language_index(
|
||||||
|
const GPtrArray *codes);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
33
include/views/person_vocabulary_adapter.h
Normal file
33
include/views/person_vocabulary_adapter.h
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#ifndef LABFY_PERSON_VOCABULARY_ADAPTER_H
|
||||||
|
#define LABFY_PERSON_VOCABULARY_ADAPTER_H
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "models/identity_traceability.h"
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
typedef struct PersonVocabularyAdapter PersonVocabularyAdapter;
|
||||||
|
PersonVocabularyAdapter *person_vocabulary_adapter_new(
|
||||||
|
Database *database, GError **error);
|
||||||
|
void person_vocabulary_adapter_free(PersonVocabularyAdapter *adapter);
|
||||||
|
const GPtrArray *person_vocabulary_adapter_get_roles(
|
||||||
|
const PersonVocabularyAdapter *adapter);
|
||||||
|
const GPtrArray *person_vocabulary_adapter_get_statuses(
|
||||||
|
const PersonVocabularyAdapter *adapter);
|
||||||
|
GtkStringList *person_vocabulary_adapter_create_role_labels(
|
||||||
|
const PersonVocabularyAdapter *adapter);
|
||||||
|
GtkStringList *person_vocabulary_adapter_create_status_labels(
|
||||||
|
const PersonVocabularyAdapter *adapter);
|
||||||
|
const char *person_vocabulary_adapter_status_code(
|
||||||
|
const PersonVocabularyAdapter *adapter, guint index);
|
||||||
|
const char *person_vocabulary_adapter_status_label(
|
||||||
|
const PersonVocabularyAdapter *adapter, guint index);
|
||||||
|
GPtrArray *person_vocabulary_adapter_selected_role_labels(
|
||||||
|
const GPtrArray *buttons);
|
||||||
|
GPtrArray *person_vocabulary_adapter_create_role_buttons(
|
||||||
|
const PersonVocabularyAdapter *adapter, GtkBox *container);
|
||||||
|
GPtrArray *person_vocabulary_adapter_build_role_assignments(
|
||||||
|
const PersonVocabularyAdapter *adapter, const GPtrArray *buttons,
|
||||||
|
const char *evidence_identifier);
|
||||||
|
gboolean person_vocabulary_adapter_justification_valid(
|
||||||
|
const PersonRoleVocabularyEntry *entry, const char *justification);
|
||||||
|
G_END_DECLS
|
||||||
|
#endif
|
||||||
|
|
@ -6116,6 +6116,7 @@ static void application_on_add_person_requested(gpointer user_data)
|
||||||
const ToolInfo *tesseract =
|
const ToolInfo *tesseract =
|
||||||
tool_registry_find(registry, "ocr.tesseract");
|
tool_registry_find(registry, "ocr.tesseract");
|
||||||
create_person_dialog_present(main_window_get_window(application->main_window),
|
create_person_dialog_present(main_window_get_window(application->main_window),
|
||||||
|
investigation_session_get_database(application->session),
|
||||||
records, investigation_project_get_root_path(project),
|
records, investigation_project_get_root_path(project),
|
||||||
application->task_manager, tesseract,
|
application->task_manager, tesseract,
|
||||||
application_person_dialog_session_matches,
|
application_person_dialog_session_matches,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "dao/evidence_entity_dao.h"
|
#include "dao/evidence_entity_dao.h"
|
||||||
#include "dao/person_role_assignment_dao.h"
|
#include "dao/person_role_assignment_dao.h"
|
||||||
#include "dao/identity_ocr_dao.h"
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "dao/identity_traceability_dao.h"
|
||||||
#include "database/transaction.h"
|
#include "database/transaction.h"
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <glib/gstdio.h>
|
#include <glib/gstdio.h>
|
||||||
|
|
@ -261,6 +262,53 @@ static gboolean persist_ocr_runs(IdentityOcrDao *dao, const char *root,
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static gboolean persist_factual_relations(IdentityTraceabilityDao *dao,
|
||||||
|
const char *person_identifier,
|
||||||
|
const PersonEvidenceSelection *selection,
|
||||||
|
const GPtrArray *evidence_identifiers,
|
||||||
|
const GPtrArray *inputs, const char *timestamp,
|
||||||
|
FailureControl *failure, GError **error)
|
||||||
|
{
|
||||||
|
for (guint i = 0; inputs != NULL && i < inputs->len; i++) {
|
||||||
|
const PersonCreationFactualRelationInput *input =
|
||||||
|
g_ptr_array_index((GPtrArray *) inputs, i);
|
||||||
|
const char *evidence_identifier = NULL;
|
||||||
|
for (guint j = 0; input != NULL &&
|
||||||
|
j < person_evidence_selection_get_count(selection); j++) {
|
||||||
|
const PersonEvidenceSelectionItem *item =
|
||||||
|
person_evidence_selection_get(selection, j);
|
||||||
|
if (g_strcmp0(
|
||||||
|
person_evidence_selection_item_get_identifier(item),
|
||||||
|
input->evidence_selection_identifier) == 0) {
|
||||||
|
evidence_identifier = g_ptr_array_index(
|
||||||
|
(GPtrArray *) evidence_identifiers, j);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char *identifier = g_uuid_string_random();
|
||||||
|
PersonEvidenceFactualRelation *relation =
|
||||||
|
input != NULL && evidence_identifier != NULL
|
||||||
|
? person_evidence_factual_relation_new(identifier,
|
||||||
|
person_identifier, evidence_identifier,
|
||||||
|
input->ocr_run_identifier, input->relation_type,
|
||||||
|
input->factual_note, timestamp, TRUE) : NULL;
|
||||||
|
g_free(identifier);
|
||||||
|
if (relation == NULL ||
|
||||||
|
fail_at(failure,
|
||||||
|
PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION, error) ||
|
||||||
|
!identity_traceability_dao_insert_factual_relation(
|
||||||
|
dao, relation, error)) {
|
||||||
|
person_evidence_factual_relation_free(relation);
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error_literal(error, coordinator_error(), 10,
|
||||||
|
"La relation factuelle explicite est invalide.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
person_evidence_factual_relation_free(relation);
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
static PersonCreationCoordinatorResult *person_creation_coordinator_execute_internal(
|
static PersonCreationCoordinatorResult *person_creation_coordinator_execute_internal(
|
||||||
Database *database, const char *root, const PersonEntityInput *person,
|
Database *database, const char *root, const PersonEntityInput *person,
|
||||||
const char *existing_person_identifier,
|
const char *existing_person_identifier,
|
||||||
|
|
@ -276,6 +324,7 @@ static PersonCreationCoordinatorResult *person_creation_coordinator_execute_inte
|
||||||
EvidenceEntityDao *link_dao = NULL;
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
PersonRoleAssignmentDao *role_dao = NULL;
|
PersonRoleAssignmentDao *role_dao = NULL;
|
||||||
IdentityOcrDao *ocr_dao = NULL;
|
IdentityOcrDao *ocr_dao = NULL;
|
||||||
|
IdentityTraceabilityDao *traceability_dao = NULL;
|
||||||
EntityRecord *person_record = NULL;
|
EntityRecord *person_record = NULL;
|
||||||
GPtrArray *created_paths = g_ptr_array_new_with_free_func(g_free);
|
GPtrArray *created_paths = g_ptr_array_new_with_free_func(g_free);
|
||||||
GPtrArray *created_directories = g_ptr_array_new_with_free_func(g_free);
|
GPtrArray *created_directories = g_ptr_array_new_with_free_func(g_free);
|
||||||
|
|
@ -319,12 +368,14 @@ static PersonCreationCoordinatorResult *person_creation_coordinator_execute_inte
|
||||||
link_dao = evidence_entity_dao_new(database, error);
|
link_dao = evidence_entity_dao_new(database, error);
|
||||||
role_dao = person_role_assignment_dao_new(database, error);
|
role_dao = person_role_assignment_dao_new(database, error);
|
||||||
ocr_dao = identity_ocr_dao_new(database);
|
ocr_dao = identity_ocr_dao_new(database);
|
||||||
|
traceability_dao = identity_traceability_dao_new(database);
|
||||||
person_record = entity_dao == NULL ? NULL : create_person
|
person_record = entity_dao == NULL ? NULL : create_person
|
||||||
? build_person(person, result->person_identifier, timestamp, error)
|
? build_person(person, result->person_identifier, timestamp, error)
|
||||||
: entity_dao_find_by_identifier(
|
: entity_dao_find_by_identifier(
|
||||||
entity_dao, result->person_identifier, error);
|
entity_dao, result->person_identifier, error);
|
||||||
if (timestamp == NULL || entity_dao == NULL || evidence_dao == NULL ||
|
if (timestamp == NULL || entity_dao == NULL || evidence_dao == NULL ||
|
||||||
link_dao == NULL || role_dao == NULL || ocr_dao == NULL ||
|
link_dao == NULL || role_dao == NULL || ocr_dao == NULL ||
|
||||||
|
traceability_dao == NULL ||
|
||||||
person_record == NULL ||
|
person_record == NULL ||
|
||||||
g_strcmp0(entity_record_get_type_identifier(person_record),
|
g_strcmp0(entity_record_get_type_identifier(person_record),
|
||||||
"person") != 0 ||
|
"person") != 0 ||
|
||||||
|
|
@ -419,6 +470,12 @@ static PersonCreationCoordinatorResult *person_creation_coordinator_execute_inte
|
||||||
created_paths, created_directories, &failure,
|
created_paths, created_directories, &failure,
|
||||||
cancellable, error))
|
cancellable, error))
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
|
if (!persist_factual_relations(traceability_dao,
|
||||||
|
result->person_identifier, selection,
|
||||||
|
result->evidence_identifiers,
|
||||||
|
options != NULL ? options->factual_relations : NULL,
|
||||||
|
timestamp, &failure, error))
|
||||||
|
goto cleanup;
|
||||||
for (guint i = 0; ocr_runs != NULL && i < ocr_runs->len; i++) {
|
for (guint i = 0; ocr_runs != NULL && i < ocr_runs->len; i++) {
|
||||||
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) ocr_runs, i);
|
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) ocr_runs, i);
|
||||||
char *directory = g_build_filename(root, "02_Preuves_Traitees",
|
char *directory = g_build_filename(root, "02_Preuves_Traitees",
|
||||||
|
|
@ -513,6 +570,7 @@ cleanup:
|
||||||
evidence_entity_dao_free(link_dao);
|
evidence_entity_dao_free(link_dao);
|
||||||
person_role_assignment_dao_free(role_dao);
|
person_role_assignment_dao_free(role_dao);
|
||||||
identity_ocr_dao_free(ocr_dao);
|
identity_ocr_dao_free(ocr_dao);
|
||||||
|
identity_traceability_dao_free(traceability_dao);
|
||||||
g_free(timestamp);
|
g_free(timestamp);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
person_creation_coordinator_result_free(result);
|
person_creation_coordinator_result_free(result);
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,12 @@
|
||||||
static gboolean person_entity_service_status_valid(const char *status)
|
static gboolean person_entity_service_status_valid(const char *status)
|
||||||
{
|
{
|
||||||
return g_strcmp0(status, "unknown") == 0 ||
|
return g_strcmp0(status, "unknown") == 0 ||
|
||||||
|
g_strcmp0(status, "unverified") == 0 ||
|
||||||
|
g_strcmp0(status, "presumed") == 0 ||
|
||||||
|
g_strcmp0(status, "partially_identified") == 0 ||
|
||||||
g_strcmp0(status, "suspected") == 0 ||
|
g_strcmp0(status, "suspected") == 0 ||
|
||||||
g_strcmp0(status, "confirmed") == 0;
|
g_strcmp0(status, "confirmed") == 0 ||
|
||||||
|
g_strcmp0(status, "disputed") == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean person_entity_service_update_display_name(Database *database,
|
gboolean person_entity_service_update_display_name(Database *database,
|
||||||
|
|
|
||||||
|
|
@ -66,24 +66,32 @@ gboolean identity_ocr_dao_insert(IdentityOcrDao*d,const char*person,
|
||||||
IdentityReviewStatus status=identity_field_observation_get_status(f);
|
IdentityReviewStatus status=identity_field_observation_get_status(f);
|
||||||
const IdentitySourceBox*b=identity_field_observation_get_box(f);char*id=g_uuid_string_random();
|
const IdentitySourceBox*b=identity_field_observation_get_box(f);char*id=g_uuid_string_random();
|
||||||
s=database_statement_prepare(d->database,"INSERT INTO identity_field_observations("
|
s=database_statement_prepare(d->database,"INSERT INTO identity_field_observations("
|
||||||
"id,observation_id,field_code,raw_value,corrected_value,confidence,review_status,"
|
"id,observation_id,field_code,raw_value,corrected_value,normalized_value,"
|
||||||
|
"confidence,review_status,"
|
||||||
"origin,evidence_id,ocr_run_id,page_number,source_x,source_y,source_width,"
|
"origin,evidence_id,ocr_run_id,page_number,source_x,source_y,source_width,"
|
||||||
"source_height,source_image_width,source_image_height,display_order,reviewed_at)"
|
"source_height,source_image_width,source_image_height,display_order,reviewed_at,"
|
||||||
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
|
"confirmed_value,confirmation_state,value_quality)"
|
||||||
|
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
|
||||||
ok=s&&bind_text(s,1,id)&&bind_text(s,2,obs)&&bind_text(s,3,identity_field_observation_get_code(f))&&
|
ok=s&&bind_text(s,1,id)&&bind_text(s,2,obs)&&bind_text(s,3,identity_field_observation_get_code(f))&&
|
||||||
bind_text(s,4,identity_field_observation_get_raw_value(f))&&
|
bind_text(s,4,identity_field_observation_get_raw_value(f))&&
|
||||||
bind_text(s,5,identity_field_observation_get_corrected_value(f))&&
|
bind_text(s,5,identity_field_observation_get_corrected_value(f))&&
|
||||||
|
bind_text(s,6,identity_field_observation_get_normalized_value(f))&&
|
||||||
(identity_field_observation_get_confidence(f)>=0?
|
(identity_field_observation_get_confidence(f)>=0?
|
||||||
database_statement_bind_double(s,6,identity_field_observation_get_confidence(f)):
|
database_statement_bind_double(s,7,identity_field_observation_get_confidence(f)):
|
||||||
database_statement_bind_null(s,6))&&bind_text(s,7,review_status_text(status))&&
|
database_statement_bind_null(s,7))&&bind_text(s,8,review_status_text(status))&&
|
||||||
bind_text(s,8,identity_field_observation_get_origin(f))&&bind_text(s,9,evidence)&&
|
bind_text(s,9,identity_field_observation_get_origin(f))&&bind_text(s,10,evidence)&&
|
||||||
bind_text(s,10,identity_ocr_run_get_identifier(r))&&database_statement_bind_int64(s,11,identity_ocr_run_get_page(r));
|
bind_text(s,11,identity_ocr_run_get_identifier(r))&&database_statement_bind_int64(s,12,identity_ocr_run_get_page(r));
|
||||||
for(int column=12;ok&&column<=17;column++){gint value=0;if(b&&b->available){
|
for(int column=13;ok&&column<=18;column++){gint value=0;if(b&&b->available){
|
||||||
const gint values[]={b->x,b->y,b->width,b->height,b->image_width,b->image_height};
|
const gint values[]={b->x,b->y,b->width,b->height,b->image_width,b->image_height};
|
||||||
value=values[column-12];ok=database_statement_bind_int64(s,column,value);
|
value=values[column-13];ok=database_statement_bind_int64(s,column,value);
|
||||||
}else ok=database_statement_bind_null(s,column);}
|
}else ok=database_statement_bind_null(s,column);}
|
||||||
ok=ok&&database_statement_bind_int64(s,18,identity_field_observation_get_order(f))&&
|
ok=ok&&database_statement_bind_int64(s,19,identity_field_observation_get_order(f))&&
|
||||||
bind_text(s,19,timestamp)&&database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
bind_text(s,20,timestamp)&&
|
||||||
|
bind_text(s,21,identity_field_observation_get_confirmed_value(f))&&
|
||||||
|
bind_text(s,22,identity_field_observation_is_human_confirmed(f)
|
||||||
|
?"human_confirmed":"unconfirmed")&&
|
||||||
|
bind_text(s,23,identity_field_observation_get_value_quality(f))&&
|
||||||
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
database_statement_finalize(s);g_free(id);if(!ok){g_free(obs);goto fail;}}
|
database_statement_finalize(s);g_free(id);if(!ok){g_free(obs);goto fail;}}
|
||||||
g_free(obs);return TRUE;
|
g_free(obs);return TRUE;
|
||||||
fail:g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),1,
|
fail:g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),1,
|
||||||
|
|
@ -111,6 +119,8 @@ void identity_document_observation_record_free(IdentityDocumentObservationRecord
|
||||||
void identity_field_observation_record_free(IdentityFieldObservationRecord*r)
|
void identity_field_observation_record_free(IdentityFieldObservationRecord*r)
|
||||||
{if(!r)return;FREE_FIELD(r,id);FREE_FIELD(r,observation_id);FREE_FIELD(r,field_code);
|
{if(!r)return;FREE_FIELD(r,id);FREE_FIELD(r,observation_id);FREE_FIELD(r,field_code);
|
||||||
FREE_FIELD(r,raw_value);FREE_FIELD(r,corrected_value);FREE_FIELD(r,normalized_value);
|
FREE_FIELD(r,raw_value);FREE_FIELD(r,corrected_value);FREE_FIELD(r,normalized_value);
|
||||||
|
FREE_FIELD(r,confirmed_value);FREE_FIELD(r,confirmation_state);
|
||||||
|
FREE_FIELD(r,value_quality);
|
||||||
FREE_FIELD(r,review_status);FREE_FIELD(r,origin);FREE_FIELD(r,evidence_id);
|
FREE_FIELD(r,review_status);FREE_FIELD(r,origin);FREE_FIELD(r,evidence_id);
|
||||||
FREE_FIELD(r,ocr_run_id);FREE_FIELD(r,reviewed_at);FREE_FIELD(r,review_note);g_free(r);}
|
FREE_FIELD(r,ocr_run_id);FREE_FIELD(r,reviewed_at);FREE_FIELD(r,review_note);g_free(r);}
|
||||||
|
|
||||||
|
|
@ -188,7 +198,10 @@ static IdentityFieldObservationRecord *read_field(DatabaseStatement*s)
|
||||||
ok=database_statement_column_int64(s,12+(int)i,values[i]);
|
ok=database_statement_column_int64(s,12+(int)i,values[i]);
|
||||||
ok=ok&&database_statement_column_int64(s,18,&r->display_order)&&
|
ok=ok&&database_statement_column_int64(s,18,&r->display_order)&&
|
||||||
database_statement_column_text(s,19,&r->reviewed_at)&&
|
database_statement_column_text(s,19,&r->reviewed_at)&&
|
||||||
database_statement_column_text(s,20,&r->review_note);
|
database_statement_column_text(s,20,&r->review_note)&&
|
||||||
|
database_statement_column_text(s,21,&r->confirmed_value)&&
|
||||||
|
database_statement_column_text(s,22,&r->confirmation_state)&&
|
||||||
|
database_statement_column_text(s,23,&r->value_quality);
|
||||||
if(!ok){identity_field_observation_record_free(r);return NULL;}return r;
|
if(!ok){identity_field_observation_record_free(r);return NULL;}return r;
|
||||||
}
|
}
|
||||||
static const char run_columns[]="id,evidence_id,expected_sha256,page_number,"
|
static const char run_columns[]="id,evidence_id,expected_sha256,page_number,"
|
||||||
|
|
@ -203,7 +216,8 @@ static const char document_columns[]="id,person_id,evidence_id,ocr_run_id,"
|
||||||
static const char field_columns[]="id,observation_id,field_code,raw_value,"
|
static const char field_columns[]="id,observation_id,field_code,raw_value,"
|
||||||
"corrected_value,normalized_value,confidence,review_status,origin,evidence_id,"
|
"corrected_value,normalized_value,confidence,review_status,origin,evidence_id,"
|
||||||
"ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
|
"ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
|
||||||
"source_image_width,source_image_height,display_order,reviewed_at,review_note";
|
"source_image_width,source_image_height,display_order,reviewed_at,review_note,"
|
||||||
|
"confirmed_value,confirmation_state,value_quality";
|
||||||
|
|
||||||
typedef gpointer(*ReadRecord)(DatabaseStatement*);
|
typedef gpointer(*ReadRecord)(DatabaseStatement*);
|
||||||
static gpointer read_run_record(DatabaseStatement*s){return read_run(s);}
|
static gpointer read_run_record(DatabaseStatement*s){return read_run(s);}
|
||||||
|
|
@ -272,6 +286,29 @@ GPtrArray *identity_ocr_dao_list_fields_by_document(IdentityOcrDao*d,const char*
|
||||||
{return list_records(d,"identity_field_observations",field_columns,
|
{return list_records(d,"identity_field_observations",field_columns,
|
||||||
"observation_id",i,read_field_record,
|
"observation_id",i,read_field_record,
|
||||||
(GDestroyNotify)identity_field_observation_record_free,e);}
|
(GDestroyNotify)identity_field_observation_record_free,e);}
|
||||||
|
GPtrArray *identity_ocr_dao_list_confirmed_fields(IdentityOcrDao*d,
|
||||||
|
const char*i,GError**e)
|
||||||
|
{
|
||||||
|
if(!d||!i){g_set_error_literal(e,g_quark_from_static_string("identity-ocr-dao"),2,
|
||||||
|
"Lecture OCR invalide.");return NULL;}
|
||||||
|
char*sql=g_strdup_printf("SELECT %s FROM identity_field_observations "
|
||||||
|
"WHERE observation_id=? AND confirmation_state='human_confirmed' "
|
||||||
|
"AND confirmed_value IS NOT NULL AND review_status IN ('accepted','modified') "
|
||||||
|
"AND value_quality IN ('complete','partial') ORDER BY display_order,rowid;",
|
||||||
|
field_columns);
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,sql);g_free(sql);
|
||||||
|
GPtrArray*a=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)identity_field_observation_record_free);
|
||||||
|
if(!s||!database_statement_bind_text(s,1,i))goto failed;
|
||||||
|
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
|
||||||
|
if(step==DATABASE_STATEMENT_STEP_DONE)break;
|
||||||
|
if(step!=DATABASE_STATEMENT_STEP_ROW)goto failed;
|
||||||
|
IdentityFieldObservationRecord*r=read_field(s);if(!r)goto failed;g_ptr_array_add(a,r);}
|
||||||
|
database_statement_finalize(s);return a;
|
||||||
|
failed:database_statement_finalize(s);g_ptr_array_unref(a);
|
||||||
|
g_set_error_literal(e,g_quark_from_static_string("identity-ocr-dao"),4,
|
||||||
|
"Impossible de lire les champs OCR confirmés.");return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
IdentityOcrRun *identity_ocr_dao_load_run(
|
IdentityOcrRun *identity_ocr_dao_load_run(
|
||||||
IdentityOcrDao*d,const char*root,const char*identifier,
|
IdentityOcrDao*d,const char*root,const char*identifier,
|
||||||
|
|
@ -329,6 +366,11 @@ IdentityOcrRun *identity_ocr_dao_load_run(
|
||||||
else if(g_strcmp0(f->review_status,"conflict")==0)
|
else if(g_strcmp0(f->review_status,"conflict")==0)
|
||||||
identity_field_observation_mark_conflict(field);
|
identity_field_observation_mark_conflict(field);
|
||||||
identity_field_observation_set_origin(field,f->origin);
|
identity_field_observation_set_origin(field,f->origin);
|
||||||
|
if(f->normalized_value!=NULL)
|
||||||
|
identity_field_observation_set_normalized_value(field,f->normalized_value);
|
||||||
|
identity_field_observation_set_value_quality(field,f->value_quality);
|
||||||
|
if(g_strcmp0(f->confirmation_state,"human_confirmed")==0)
|
||||||
|
identity_field_observation_confirm(field,f->confirmed_value);
|
||||||
identity_ocr_run_add_field(run,field);
|
identity_ocr_run_add_field(run,field);
|
||||||
}
|
}
|
||||||
g_clear_pointer(&fields,g_ptr_array_unref);
|
g_clear_pointer(&fields,g_ptr_array_unref);
|
||||||
|
|
@ -354,34 +396,40 @@ static gboolean update_review_fields(IdentityOcrDao*d,const char*observation,
|
||||||
char*id=g_uuid_string_random();
|
char*id=g_uuid_string_random();
|
||||||
DatabaseStatement*s=database_statement_prepare(d->database,
|
DatabaseStatement*s=database_statement_prepare(d->database,
|
||||||
"INSERT INTO identity_field_observations(id,observation_id,field_code,"
|
"INSERT INTO identity_field_observations(id,observation_id,field_code,"
|
||||||
"raw_value,corrected_value,confidence,review_status,origin,evidence_id,"
|
"raw_value,corrected_value,normalized_value,confidence,review_status,origin,evidence_id,"
|
||||||
"ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
|
"ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
|
||||||
"source_image_width,source_image_height,display_order,reviewed_at)"
|
"source_image_width,source_image_height,display_order,reviewed_at,"
|
||||||
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
|
"confirmed_value,confirmation_state,value_quality)"
|
||||||
|
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
|
||||||
gboolean ok=s&&bind_text(s,1,id)&&bind_text(s,2,observation)&&
|
gboolean ok=s&&bind_text(s,1,id)&&bind_text(s,2,observation)&&
|
||||||
bind_text(s,3,identity_field_observation_get_code(f))&&
|
bind_text(s,3,identity_field_observation_get_code(f))&&
|
||||||
bind_text(s,4,identity_field_observation_get_raw_value(f))&&
|
bind_text(s,4,identity_field_observation_get_raw_value(f))&&
|
||||||
bind_text(s,5,identity_field_observation_get_corrected_value(f))&&
|
bind_text(s,5,identity_field_observation_get_corrected_value(f))&&
|
||||||
|
bind_text(s,6,identity_field_observation_get_normalized_value(f))&&
|
||||||
(identity_field_observation_get_confidence(f)>=0
|
(identity_field_observation_get_confidence(f)>=0
|
||||||
?database_statement_bind_double(s,6,
|
?database_statement_bind_double(s,7,
|
||||||
identity_field_observation_get_confidence(f))
|
identity_field_observation_get_confidence(f))
|
||||||
:database_statement_bind_null(s,6))&&
|
:database_statement_bind_null(s,7))&&
|
||||||
bind_text(s,7,review_status_text(
|
bind_text(s,8,review_status_text(
|
||||||
identity_field_observation_get_status(f)))&&
|
identity_field_observation_get_status(f)))&&
|
||||||
bind_text(s,8,identity_field_observation_get_origin(f))&&
|
bind_text(s,9,identity_field_observation_get_origin(f))&&
|
||||||
bind_text(s,9,identity_ocr_run_get_evidence_id(run))&&
|
bind_text(s,10,identity_ocr_run_get_evidence_id(run))&&
|
||||||
bind_text(s,10,identity_ocr_run_get_identifier(run))&&
|
bind_text(s,11,identity_ocr_run_get_identifier(run))&&
|
||||||
database_statement_bind_int64(s,11,identity_ocr_run_get_page(run));
|
database_statement_bind_int64(s,12,identity_ocr_run_get_page(run));
|
||||||
const gint values[]={b!=NULL?b->x:0,b!=NULL?b->y:0,
|
const gint values[]={b!=NULL?b->x:0,b!=NULL?b->y:0,
|
||||||
b!=NULL?b->width:0,b!=NULL?b->height:0,
|
b!=NULL?b->width:0,b!=NULL?b->height:0,
|
||||||
b!=NULL?b->image_width:0,b!=NULL?b->image_height:0};
|
b!=NULL?b->image_width:0,b!=NULL?b->image_height:0};
|
||||||
for(gint column=12;ok&&column<=17;column++)
|
for(gint column=13;ok&&column<=18;column++)
|
||||||
ok=b!=NULL&&b->available
|
ok=b!=NULL&&b->available
|
||||||
?database_statement_bind_int64(s,column,values[column-12])
|
?database_statement_bind_int64(s,column,values[column-13])
|
||||||
:database_statement_bind_null(s,column);
|
:database_statement_bind_null(s,column);
|
||||||
ok=ok&&database_statement_bind_int64(s,18,
|
ok=ok&&database_statement_bind_int64(s,19,
|
||||||
identity_field_observation_get_order(f))&&
|
identity_field_observation_get_order(f))&&
|
||||||
bind_text(s,19,timestamp)&&
|
bind_text(s,20,timestamp)&&
|
||||||
|
bind_text(s,21,identity_field_observation_get_confirmed_value(f))&&
|
||||||
|
bind_text(s,22,identity_field_observation_is_human_confirmed(f)
|
||||||
|
?"human_confirmed":"unconfirmed")&&
|
||||||
|
bind_text(s,23,identity_field_observation_get_value_quality(f))&&
|
||||||
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
database_statement_finalize(s);g_free(id);if(!ok)return FALSE;
|
database_statement_finalize(s);g_free(id);if(!ok)return FALSE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
184
src/dao/identity_traceability_dao.c
Normal file
184
src/dao/identity_traceability_dao.c
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
#include "dao/identity_traceability_dao.h"
|
||||||
|
#include "database/statement.h"
|
||||||
|
struct IdentityTraceabilityDao{Database*database;};
|
||||||
|
static GQuark domain(void){return g_quark_from_static_string("identity-traceability-dao");}
|
||||||
|
static void fail(GError**e,const char*m){if(e&&*e==NULL)g_set_error_literal(e,domain(),1,m);}
|
||||||
|
static gboolean bind(DatabaseStatement*s,int i,const char*v)
|
||||||
|
{return v?database_statement_bind_text(s,i,v):database_statement_bind_null(s,i);}
|
||||||
|
IdentityTraceabilityDao *identity_traceability_dao_new(Database*d)
|
||||||
|
{if(!d)return NULL;IdentityTraceabilityDao*dao=g_new0(IdentityTraceabilityDao,1);
|
||||||
|
dao->database=d;return dao;}
|
||||||
|
void identity_traceability_dao_free(IdentityTraceabilityDao*d){g_free(d);}
|
||||||
|
|
||||||
|
gboolean identity_traceability_dao_insert_authenticity(
|
||||||
|
IdentityTraceabilityDao*d,const DocumentAuthenticityAssessment*a,GError**e)
|
||||||
|
{
|
||||||
|
DocumentAuthenticityAssessment*valid=a?document_authenticity_assessment_copy(a):NULL;
|
||||||
|
if(!d||!valid){fail(e,"Évaluation d’authenticité invalide.");return FALSE;}
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,
|
||||||
|
"INSERT INTO document_authenticity_assessments VALUES(?,?,?,?,?,?,?,?,?);");
|
||||||
|
gboolean ok=s&&bind(s,1,a->identifier)&&bind(s,2,a->evidence_identifier)&&
|
||||||
|
bind(s,3,a->ocr_run_identifier)&&bind(s,4,a->status)&&bind(s,5,a->justification)&&
|
||||||
|
bind(s,6,a->assessed_at)&&bind(s,7,a->previous_identifier)&&
|
||||||
|
bind(s,8,a->technical_note)&&bind(s,9,"human")&&
|
||||||
|
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(s);document_authenticity_assessment_free(valid);
|
||||||
|
if(!ok)fail(e,"Impossible de conserver l’évaluation d’authenticité.");
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
static DocumentAuthenticityAssessment *read_auth(DatabaseStatement*s)
|
||||||
|
{
|
||||||
|
char *v[9]={0};gboolean ok=TRUE;
|
||||||
|
for(int i=0;i<9;i++)ok=ok&&database_statement_column_text(s,i,&v[i]);
|
||||||
|
DocumentAuthenticityAssessment*a=ok?document_authenticity_assessment_new(
|
||||||
|
v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7]):NULL;
|
||||||
|
for(int i=0;i<9;i++)g_free(v[i]);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
static GPtrArray *auth_query(IdentityTraceabilityDao*d,const char*sql,
|
||||||
|
const char*value,GError**e)
|
||||||
|
{
|
||||||
|
if(!d||!value){fail(e,"Lecture d’authenticité invalide.");return NULL;}
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,sql);
|
||||||
|
GPtrArray*a=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)document_authenticity_assessment_free);
|
||||||
|
if(!s||!bind(s,1,value))goto bad;
|
||||||
|
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
|
||||||
|
if(step==DATABASE_STATEMENT_STEP_DONE)break;
|
||||||
|
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad;
|
||||||
|
DocumentAuthenticityAssessment*r=read_auth(s);if(!r)goto bad;g_ptr_array_add(a,r);}
|
||||||
|
database_statement_finalize(s);return a;
|
||||||
|
bad:database_statement_finalize(s);g_ptr_array_unref(a);
|
||||||
|
fail(e,"Impossible de lire l’historique d’authenticité.");return NULL;
|
||||||
|
}
|
||||||
|
DocumentAuthenticityAssessment *identity_traceability_dao_find_authenticity(
|
||||||
|
IdentityTraceabilityDao*d,const char*id,GError**e)
|
||||||
|
{
|
||||||
|
GPtrArray*a=auth_query(d,"SELECT * FROM document_authenticity_assessments "
|
||||||
|
"WHERE id=? ORDER BY assessed_at,id;",id,e);if(!a)return NULL;
|
||||||
|
DocumentAuthenticityAssessment*r=a->len?document_authenticity_assessment_copy(
|
||||||
|
g_ptr_array_index(a,0)):NULL;g_ptr_array_unref(a);return r;
|
||||||
|
}
|
||||||
|
GPtrArray *identity_traceability_dao_list_authenticity(
|
||||||
|
IdentityTraceabilityDao*d,const char*id,GError**e)
|
||||||
|
{return auth_query(d,"SELECT * FROM document_authenticity_assessments "
|
||||||
|
"WHERE evidence_id=? ORDER BY assessed_at,id;",id,e);}
|
||||||
|
DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity(
|
||||||
|
IdentityTraceabilityDao*d,const char*id,GError**e)
|
||||||
|
{
|
||||||
|
GPtrArray*a=auth_query(d,"SELECT * FROM document_authenticity_assessments "
|
||||||
|
"WHERE evidence_id=? ORDER BY assessed_at DESC,id DESC LIMIT 1;",id,e);
|
||||||
|
if(!a)return NULL;
|
||||||
|
DocumentAuthenticityAssessment*r=a->len?
|
||||||
|
document_authenticity_assessment_copy(g_ptr_array_index(a,0)):NULL;
|
||||||
|
g_ptr_array_unref(a);return r;
|
||||||
|
}
|
||||||
|
gboolean identity_traceability_dao_insert_factual_relation(
|
||||||
|
IdentityTraceabilityDao*d,const PersonEvidenceFactualRelation*r,GError**e)
|
||||||
|
{
|
||||||
|
PersonEvidenceFactualRelation*valid=r?person_evidence_factual_relation_copy(r):NULL;
|
||||||
|
if(!d||!valid){fail(e,"Relation factuelle invalide.");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)&&
|
||||||
|
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.");
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
GPtrArray *identity_traceability_dao_list_factual_relations(
|
||||||
|
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 evidence_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 bad;
|
||||||
|
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
|
||||||
|
if(step==DATABASE_STATEMENT_STEP_DONE)break;
|
||||||
|
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad;
|
||||||
|
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 bad;
|
||||||
|
g_ptr_array_add(a,r);}
|
||||||
|
database_statement_finalize(s);return a;
|
||||||
|
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_roles(
|
||||||
|
IdentityTraceabilityDao*d,gboolean inactive,GError**e)
|
||||||
|
{
|
||||||
|
if(!d){fail(e,"Lecture du vocabulaire invalide.");return NULL;}
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,
|
||||||
|
inactive?"SELECT code,label,description,display_order,active,"
|
||||||
|
"requires_justification,sensitive FROM person_role_vocabulary "
|
||||||
|
"ORDER BY display_order,code;":"SELECT code,label,description,display_order,"
|
||||||
|
"active,requires_justification,sensitive FROM person_role_vocabulary "
|
||||||
|
"WHERE active=1 ORDER BY display_order,code;");
|
||||||
|
GPtrArray*a=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)person_role_vocabulary_entry_free);
|
||||||
|
if(!s)goto bad;
|
||||||
|
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
|
||||||
|
if(step==DATABASE_STATEMENT_STEP_DONE)break;
|
||||||
|
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad;
|
||||||
|
PersonRoleVocabularyEntry*r=g_new0(PersonRoleVocabularyEntry,1);
|
||||||
|
gint64 order=0,active=0,required=0,sensitive=0;
|
||||||
|
gboolean ok=database_statement_column_text(s,0,&r->code)&&
|
||||||
|
database_statement_column_text(s,1,&r->label)&&
|
||||||
|
database_statement_column_text(s,2,&r->description)&&
|
||||||
|
database_statement_column_int64(s,3,&order)&&
|
||||||
|
database_statement_column_int64(s,4,&active)&&
|
||||||
|
database_statement_column_int64(s,5,&required)&&
|
||||||
|
database_statement_column_int64(s,6,&sensitive);
|
||||||
|
if(!ok){person_role_vocabulary_entry_free(r);goto bad;}
|
||||||
|
r->display_order=(gint)order;r->active=active!=0;
|
||||||
|
r->requires_justification=required!=0;r->sensitive=sensitive!=0;
|
||||||
|
g_ptr_array_add(a,r);}database_statement_finalize(s);return a;
|
||||||
|
bad:database_statement_finalize(s);g_ptr_array_unref(a);
|
||||||
|
fail(e,"Impossible de lire le vocabulaire des rôles.");return NULL;
|
||||||
|
}
|
||||||
|
GPtrArray *identity_traceability_dao_list_identification_statuses(
|
||||||
|
IdentityTraceabilityDao*d,gboolean inactive,GError**e)
|
||||||
|
{
|
||||||
|
if(!d){fail(e,"Lecture du vocabulaire invalide.");return NULL;}
|
||||||
|
DatabaseStatement*s=database_statement_prepare(d->database,
|
||||||
|
inactive?"SELECT code,label,description,display_order,active,"
|
||||||
|
"requires_justification,sensitive FROM identification_status_vocabulary "
|
||||||
|
"ORDER BY display_order,code;":"SELECT code,label,description,display_order,"
|
||||||
|
"active,requires_justification,sensitive FROM identification_status_vocabulary "
|
||||||
|
"WHERE active=1 ORDER BY display_order,code;");
|
||||||
|
GPtrArray*a=g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify)identification_status_vocabulary_entry_free);
|
||||||
|
if(!s)goto bad;
|
||||||
|
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
|
||||||
|
if(step==DATABASE_STATEMENT_STEP_DONE)break;
|
||||||
|
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad;
|
||||||
|
IdentificationStatusVocabularyEntry*r=g_new0(
|
||||||
|
IdentificationStatusVocabularyEntry,1);
|
||||||
|
gint64 order=0,active=0,required=0,sensitive=0;
|
||||||
|
gboolean ok=database_statement_column_text(s,0,&r->code)&&
|
||||||
|
database_statement_column_text(s,1,&r->label)&&
|
||||||
|
database_statement_column_text(s,2,&r->description)&&
|
||||||
|
database_statement_column_int64(s,3,&order)&&
|
||||||
|
database_statement_column_int64(s,4,&active)&&
|
||||||
|
database_statement_column_int64(s,5,&required)&&
|
||||||
|
database_statement_column_int64(s,6,&sensitive);
|
||||||
|
if(!ok){identification_status_vocabulary_entry_free(r);goto bad;}
|
||||||
|
r->display_order=(gint)order;r->active=active!=0;
|
||||||
|
r->requires_justification=required!=0;r->sensitive=sensitive!=0;
|
||||||
|
g_ptr_array_add(a,r);}
|
||||||
|
database_statement_finalize(s);return a;
|
||||||
|
bad:database_statement_finalize(s);g_ptr_array_unref(a);
|
||||||
|
fail(e,"Impossible de lire le vocabulaire des états d’identification.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,8 @@ gboolean person_role_assignment_dao_insert(PersonRoleAssignmentDao *dao,
|
||||||
"SELECT ?,?,?,?,?,?,?,?,? WHERE EXISTS(SELECT 1 FROM entites e "
|
"SELECT ?,?,?,?,?,?,?,?,? WHERE EXISTS(SELECT 1 FROM entites e "
|
||||||
"JOIN types_entite t ON t.id=e.type_id WHERE e.id=? AND t.code='person') "
|
"JOIN types_entite t ON t.id=e.type_id WHERE e.id=? AND t.code='person') "
|
||||||
"AND (? IS NULL OR EXISTS(SELECT 1 FROM preuves WHERE id=?)) "
|
"AND (? IS NULL OR EXISTS(SELECT 1 FROM preuves WHERE id=?)) "
|
||||||
|
"AND EXISTS(SELECT 1 FROM person_role_vocabulary v "
|
||||||
|
"WHERE v.code=? AND v.active=1) "
|
||||||
"RETURNING id;";
|
"RETURNING id;";
|
||||||
DatabaseStatement *statement = NULL;
|
DatabaseStatement *statement = NULL;
|
||||||
GDateTime *now = NULL;
|
GDateTime *now = NULL;
|
||||||
|
|
@ -65,6 +67,7 @@ gboolean person_role_assignment_dao_insert(PersonRoleAssignmentDao *dao,
|
||||||
!(input->evidence_identifier != NULL
|
!(input->evidence_identifier != NULL
|
||||||
? database_statement_bind_text(statement, 12, input->evidence_identifier)
|
? database_statement_bind_text(statement, 12, input->evidence_identifier)
|
||||||
: database_statement_bind_null(statement, 12)) ||
|
: database_statement_bind_null(statement, 12)) ||
|
||||||
|
!database_statement_bind_text(statement, 13, input->role_code) ||
|
||||||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW)
|
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW)
|
||||||
goto failed;
|
goto failed;
|
||||||
success = TRUE;
|
success = TRUE;
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle du schéma SQLite.
|
* @brief Version actuelle du schéma SQLite.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT 17
|
#define DATABASE_SCHEMA_VERSION_CURRENT 18
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "17"
|
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "18"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||||
|
|
@ -917,6 +917,21 @@ rollback:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool database_migrate_v17_to_v18(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v18(database) ||
|
||||||
|
!database_update_schema_version(database, "18") ||
|
||||||
|
!database_transaction_commit(database)) goto rollback;
|
||||||
|
return true;
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V17 vers V18.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1174,6 +1189,10 @@ bool database_migrate_to_latest(
|
||||||
if (!database_migrate_v16_to_v17(database)) return false;
|
if (!database_migrate_v16_to_v17(database)) return false;
|
||||||
schema_version = 17;
|
schema_version = 17;
|
||||||
break;
|
break;
|
||||||
|
case 17:
|
||||||
|
if (!database_migrate_v17_to_v18(database)) return false;
|
||||||
|
schema_version = 18;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
database_set_error(
|
database_set_error(
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,32 @@ bool schema_install_v17(Database *database)
|
||||||
"la migration SQLite V17");
|
"la migration SQLite V17");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool schema_install_v18(Database *database)
|
||||||
|
{
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
sqlite3 *handle = database_get_handle(database);
|
||||||
|
if (handle != NULL &&
|
||||||
|
sqlite3_prepare_v2(handle,
|
||||||
|
"SELECT confirmed_value,confirmation_state,value_quality "
|
||||||
|
"FROM identity_field_observations LIMIT 0",
|
||||||
|
-1, &statement, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
statement = NULL;
|
||||||
|
if (sqlite3_prepare_v2(handle,
|
||||||
|
"SELECT 1 FROM document_authenticity_assessments,"
|
||||||
|
"person_evidence_factual_relations,"
|
||||||
|
"identification_status_vocabulary,"
|
||||||
|
"person_role_vocabulary LIMIT 0",
|
||||||
|
-1, &statement, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(statement);
|
||||||
|
return schema_execute_file(database, "database/schema_v18.sql",
|
||||||
|
"la migration SQLite V18");
|
||||||
|
}
|
||||||
|
|
||||||
bool schema_ensure_current(
|
bool schema_ensure_current(
|
||||||
Database *database
|
Database *database
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "models/identity_ocr.h"
|
#include "models/identity_ocr.h"
|
||||||
struct IdentityFieldObservation {
|
struct IdentityFieldObservation {
|
||||||
char *code, *raw_value, *corrected_value, *normalized_value, *note;
|
char *code, *raw_value, *corrected_value, *normalized_value, *note;
|
||||||
|
char *confirmed_value, *value_quality;
|
||||||
char *origin;
|
char *origin;
|
||||||
double confidence;
|
double confidence;
|
||||||
IdentityReviewStatus status;
|
IdentityReviewStatus status;
|
||||||
|
|
@ -48,7 +49,7 @@ IdentityFieldObservation *identity_field_observation_new(
|
||||||
IdentityFieldObservation *f=g_new0(IdentityFieldObservation,1);
|
IdentityFieldObservation *f=g_new0(IdentityFieldObservation,1);
|
||||||
f->code=g_strdup(code); f->raw_value=g_strdup(raw);
|
f->code=g_strdup(code); f->raw_value=g_strdup(raw);
|
||||||
f->confidence=confidence; f->status=IDENTITY_REVIEW_PROPOSED;
|
f->confidence=confidence; f->status=IDENTITY_REVIEW_PROPOSED;
|
||||||
f->origin=g_strdup("ocr"); f->order=order;
|
f->origin=g_strdup("ocr"); f->value_quality=g_strdup("complete"); f->order=order;
|
||||||
if (box != NULL) f->box=*box;
|
if (box != NULL) f->box=*box;
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +65,7 @@ IdentityFieldObservation *identity_field_observation_new_manual(
|
||||||
field->confidence = -1.0;
|
field->confidence = -1.0;
|
||||||
field->status = IDENTITY_REVIEW_PROPOSED;
|
field->status = IDENTITY_REVIEW_PROPOSED;
|
||||||
field->origin = g_strdup("manual_entry");
|
field->origin = g_strdup("manual_entry");
|
||||||
|
field->value_quality = g_strdup("complete");
|
||||||
field->order = order;
|
field->order = order;
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +80,8 @@ IdentityFieldObservation *identity_field_observation_copy(
|
||||||
f->code,f->corrected_value,f->order);
|
f->code,f->corrected_value,f->order);
|
||||||
c->corrected_value=g_strdup(f->corrected_value);
|
c->corrected_value=g_strdup(f->corrected_value);
|
||||||
c->normalized_value=g_strdup(f->normalized_value);
|
c->normalized_value=g_strdup(f->normalized_value);
|
||||||
|
c->confirmed_value=g_strdup(f->confirmed_value);
|
||||||
|
g_free(c->value_quality);c->value_quality=g_strdup(f->value_quality);
|
||||||
c->note=g_strdup(f->note); c->status=f->status;
|
c->note=g_strdup(f->note); c->status=f->status;
|
||||||
g_free(c->origin); c->origin=g_strdup(f->origin); return c;
|
g_free(c->origin); c->origin=g_strdup(f->origin); return c;
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +90,7 @@ void identity_field_observation_free(IdentityFieldObservation *f)
|
||||||
if(f==NULL)return;
|
if(f==NULL)return;
|
||||||
g_free(f->code);g_free(f->raw_value);
|
g_free(f->code);g_free(f->raw_value);
|
||||||
g_free(f->corrected_value);g_free(f->normalized_value);g_free(f->note);
|
g_free(f->corrected_value);g_free(f->normalized_value);g_free(f->note);
|
||||||
g_free(f->origin);g_free(f);
|
g_free(f->origin);g_free(f->confirmed_value);g_free(f->value_quality);g_free(f);
|
||||||
}
|
}
|
||||||
gboolean identity_field_observation_accept(IdentityFieldObservation *f)
|
gboolean identity_field_observation_accept(IdentityFieldObservation *f)
|
||||||
{ if(f==NULL)return FALSE;f->status=IDENTITY_REVIEW_ACCEPTED;return TRUE; }
|
{ if(f==NULL)return FALSE;f->status=IDENTITY_REVIEW_ACCEPTED;return TRUE; }
|
||||||
|
|
@ -112,7 +116,8 @@ gboolean identity_field_observation_restore_raw(IdentityFieldObservation *f)
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
void identity_field_observation_reject(IdentityFieldObservation *f)
|
void identity_field_observation_reject(IdentityFieldObservation *f)
|
||||||
{if(f!=NULL)f->status=IDENTITY_REVIEW_REJECTED;}
|
{if(f!=NULL){f->status=IDENTITY_REVIEW_REJECTED;
|
||||||
|
g_clear_pointer(&f->confirmed_value,g_free);}}
|
||||||
gboolean identity_field_observation_set_origin(IdentityFieldObservation*f,
|
gboolean identity_field_observation_set_origin(IdentityFieldObservation*f,
|
||||||
const char*origin)
|
const char*origin)
|
||||||
{if(f==NULL||(!g_str_equal(origin,"ocr")&&!g_str_equal(origin,"mrz")&&
|
{if(f==NULL||(!g_str_equal(origin,"ocr")&&!g_str_equal(origin,"mrz")&&
|
||||||
|
|
@ -120,17 +125,44 @@ gboolean identity_field_observation_set_origin(IdentityFieldObservation*f,
|
||||||
return FALSE;
|
return FALSE;
|
||||||
g_free(f->origin);f->origin=g_strdup(origin);return TRUE;}
|
g_free(f->origin);f->origin=g_strdup(origin);return TRUE;}
|
||||||
void identity_field_observation_mark_conflict(IdentityFieldObservation*f)
|
void identity_field_observation_mark_conflict(IdentityFieldObservation*f)
|
||||||
{if(f!=NULL)f->status=IDENTITY_REVIEW_CONFLICT;}
|
{if(f!=NULL){f->status=IDENTITY_REVIEW_CONFLICT;
|
||||||
|
g_clear_pointer(&f->confirmed_value,g_free);}}
|
||||||
#define FG(name,type,field,zero) type identity_field_observation_get_##name(\
|
#define FG(name,type,field,zero) type identity_field_observation_get_##name(\
|
||||||
const IdentityFieldObservation*f){return f!=NULL?f->field:zero;}
|
const IdentityFieldObservation*f){return f!=NULL?f->field:zero;}
|
||||||
FG(code,const char*,code,NULL) FG(raw_value,const char*,raw_value,NULL)
|
FG(code,const char*,code,NULL) FG(raw_value,const char*,raw_value,NULL)
|
||||||
FG(corrected_value,const char*,corrected_value,NULL)
|
FG(corrected_value,const char*,corrected_value,NULL)
|
||||||
|
FG(normalized_value,const char*,normalized_value,NULL)
|
||||||
|
FG(confirmed_value,const char*,confirmed_value,NULL)
|
||||||
|
FG(value_quality,const char*,value_quality,NULL)
|
||||||
FG(origin,const char*,origin,NULL)
|
FG(origin,const char*,origin,NULL)
|
||||||
FG(status,IdentityReviewStatus,status,IDENTITY_REVIEW_PROPOSED)
|
FG(status,IdentityReviewStatus,status,IDENTITY_REVIEW_PROPOSED)
|
||||||
FG(confidence,double,confidence,-1.0)
|
FG(confidence,double,confidence,-1.0)
|
||||||
FG(order,guint,order,0)
|
FG(order,guint,order,0)
|
||||||
const IdentitySourceBox *identity_field_observation_get_box(
|
const IdentitySourceBox *identity_field_observation_get_box(
|
||||||
const IdentityFieldObservation*f){return f!=NULL?&f->box:NULL;}
|
const IdentityFieldObservation*f){return f!=NULL?&f->box:NULL;}
|
||||||
|
gboolean identity_field_observation_set_normalized_value(
|
||||||
|
IdentityFieldObservation*f,const char*v)
|
||||||
|
{if(!f||!v||!v[0]||!g_utf8_validate(v,-1,NULL))return FALSE;
|
||||||
|
g_free(f->normalized_value);f->normalized_value=g_strdup(v);return TRUE;}
|
||||||
|
gboolean identity_field_observation_set_value_quality(
|
||||||
|
IdentityFieldObservation*f,const char*q)
|
||||||
|
{if(!f||(!g_str_equal(q,"complete")&&!g_str_equal(q,"partial")&&
|
||||||
|
!g_str_equal(q,"uncertain")&&!g_str_equal(q,"invalid")))return FALSE;
|
||||||
|
g_free(f->value_quality);f->value_quality=g_strdup(q);
|
||||||
|
if(g_str_equal(q,"uncertain")||g_str_equal(q,"invalid"))
|
||||||
|
g_clear_pointer(&f->confirmed_value,g_free);
|
||||||
|
return TRUE;}
|
||||||
|
gboolean identity_field_observation_confirm(IdentityFieldObservation*f,
|
||||||
|
const char*v)
|
||||||
|
{if(!f||!v||!v[0]||f->status==IDENTITY_REVIEW_REJECTED||
|
||||||
|
f->status==IDENTITY_REVIEW_CONFLICT||
|
||||||
|
g_strcmp0(f->value_quality,"uncertain")==0||
|
||||||
|
g_strcmp0(f->value_quality,"invalid")==0)return FALSE;
|
||||||
|
g_free(f->confirmed_value);f->confirmed_value=g_strdup(v);return TRUE;}
|
||||||
|
void identity_field_observation_clear_confirmation(IdentityFieldObservation*f)
|
||||||
|
{if(f)g_clear_pointer(&f->confirmed_value,g_free);}
|
||||||
|
gboolean identity_field_observation_is_human_confirmed(
|
||||||
|
const IdentityFieldObservation*f){return f&&f->confirmed_value!=NULL;}
|
||||||
IdentityOcrRun *identity_ocr_run_new(const char *evidence_id,
|
IdentityOcrRun *identity_ocr_run_new(const char *evidence_id,
|
||||||
const char *sha,const char *type,const char *side,guint page,
|
const char *sha,const char *type,const char *side,guint page,
|
||||||
const char *languages,const char *profile)
|
const char *languages,const char *profile)
|
||||||
|
|
|
||||||
109
src/models/identity_traceability.c
Normal file
109
src/models/identity_traceability.c
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
#include "models/identity_traceability.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static gboolean in_values(const char *value, const char *const *values,
|
||||||
|
gsize count)
|
||||||
|
{
|
||||||
|
if (value == NULL) return FALSE;
|
||||||
|
for (gsize i = 0; i < count; i++)
|
||||||
|
if (g_str_equal(value, values[i])) return TRUE;
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
static gboolean text(const char *value)
|
||||||
|
{ return value != NULL && value[0] != '\0' && g_utf8_validate(value,-1,NULL); }
|
||||||
|
static gboolean optional_text(const char *value)
|
||||||
|
{ return value == NULL || (text(value) && strlen(value) <= 65536); }
|
||||||
|
static gboolean uuid_or_null(const char *value)
|
||||||
|
{ return value == NULL || g_uuid_string_is_valid(value); }
|
||||||
|
static gboolean timestamp(const char *value)
|
||||||
|
{ return value != NULL && strlen(value) == 20; }
|
||||||
|
|
||||||
|
gboolean identity_traceability_authenticity_status_valid(const char *value)
|
||||||
|
{
|
||||||
|
static const char *const values[]={"indeterminate","presumed_authentic",
|
||||||
|
"suspicious","presumed_forged","confirmed_forged"};
|
||||||
|
return in_values(value,values,G_N_ELEMENTS(values));
|
||||||
|
}
|
||||||
|
gboolean identity_traceability_relation_type_valid(const char *value)
|
||||||
|
{
|
||||||
|
static const char *const values[]={"identity_observed_in",
|
||||||
|
"document_presented_in_name_of","declared_holder_in",
|
||||||
|
"data_extracted_from"};
|
||||||
|
return in_values(value,values,G_N_ELEMENTS(values));
|
||||||
|
}
|
||||||
|
gboolean identity_traceability_identification_status_valid(const char *value)
|
||||||
|
{
|
||||||
|
static const char *const values[]={"unknown","unverified","presumed",
|
||||||
|
"partially_identified","confirmed","disputed"};
|
||||||
|
return in_values(value,values,G_N_ELEMENTS(values));
|
||||||
|
}
|
||||||
|
gboolean identity_traceability_value_quality_valid(const char *value)
|
||||||
|
{
|
||||||
|
static const char *const values[]={"complete","partial","uncertain","invalid"};
|
||||||
|
return in_values(value,values,G_N_ELEMENTS(values));
|
||||||
|
}
|
||||||
|
gboolean identity_traceability_field_is_projectable(const char *review,
|
||||||
|
const char *quality,const char *confirmation,const char *confirmed)
|
||||||
|
{
|
||||||
|
return confirmed != NULL && confirmed[0] != '\0' &&
|
||||||
|
g_strcmp0(confirmation,"human_confirmed")==0 &&
|
||||||
|
(g_strcmp0(review,"accepted")==0||g_strcmp0(review,"modified")==0) &&
|
||||||
|
(g_strcmp0(quality,"complete")==0||g_strcmp0(quality,"partial")==0);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentAuthenticityAssessment *document_authenticity_assessment_new(
|
||||||
|
const char *id,const char *evidence,const char *run,const char *status,
|
||||||
|
const char *justification,const char *at,const char *previous,const char *note)
|
||||||
|
{
|
||||||
|
if(!g_uuid_string_is_valid(id)||!g_uuid_string_is_valid(evidence)||
|
||||||
|
!uuid_or_null(run)||!uuid_or_null(previous)||
|
||||||
|
!identity_traceability_authenticity_status_valid(status)||
|
||||||
|
!timestamp(at)||!optional_text(justification)||!optional_text(note)||
|
||||||
|
(g_strcmp0(status,"indeterminate")!=0&&!text(justification)))return NULL;
|
||||||
|
DocumentAuthenticityAssessment*a=g_new0(DocumentAuthenticityAssessment,1);
|
||||||
|
a->identifier=g_strdup(id);a->evidence_identifier=g_strdup(evidence);
|
||||||
|
a->ocr_run_identifier=g_strdup(run);a->status=g_strdup(status);
|
||||||
|
a->justification=g_strdup(justification);a->assessed_at=g_strdup(at);
|
||||||
|
a->previous_identifier=g_strdup(previous);a->technical_note=g_strdup(note);
|
||||||
|
a->origin=g_strdup("human");return a;
|
||||||
|
}
|
||||||
|
DocumentAuthenticityAssessment *document_authenticity_assessment_copy(
|
||||||
|
const DocumentAuthenticityAssessment*a)
|
||||||
|
{return a?document_authenticity_assessment_new(a->identifier,
|
||||||
|
a->evidence_identifier,a->ocr_run_identifier,a->status,a->justification,
|
||||||
|
a->assessed_at,a->previous_identifier,a->technical_note):NULL;}
|
||||||
|
void document_authenticity_assessment_free(DocumentAuthenticityAssessment*a)
|
||||||
|
{if(!a)return;g_free(a->identifier);g_free(a->evidence_identifier);
|
||||||
|
g_free(a->ocr_run_identifier);g_free(a->status);g_free(a->justification);
|
||||||
|
g_free(a->assessed_at);g_free(a->previous_identifier);
|
||||||
|
g_free(a->technical_note);g_free(a->origin);g_free(a);}
|
||||||
|
|
||||||
|
PersonEvidenceFactualRelation *person_evidence_factual_relation_new(
|
||||||
|
const char *id,const char *person,const char *evidence,const char *run,
|
||||||
|
const char *type,const char *note,const char *at,gboolean active)
|
||||||
|
{
|
||||||
|
if(!g_uuid_string_is_valid(id)||!g_uuid_string_is_valid(person)||
|
||||||
|
!g_uuid_string_is_valid(evidence)||!uuid_or_null(run)||
|
||||||
|
!identity_traceability_relation_type_valid(type)||!optional_text(note)||
|
||||||
|
!timestamp(at))return NULL;
|
||||||
|
PersonEvidenceFactualRelation*r=g_new0(PersonEvidenceFactualRelation,1);
|
||||||
|
r->identifier=g_strdup(id);r->person_identifier=g_strdup(person);
|
||||||
|
r->evidence_identifier=g_strdup(evidence);r->ocr_run_identifier=g_strdup(run);
|
||||||
|
r->relation_type=g_strdup(type);r->factual_note=g_strdup(note);
|
||||||
|
r->observed_at=g_strdup(at);r->origin=g_strdup("human");r->active=active;return r;
|
||||||
|
}
|
||||||
|
PersonEvidenceFactualRelation *person_evidence_factual_relation_copy(
|
||||||
|
const PersonEvidenceFactualRelation*r)
|
||||||
|
{return r?person_evidence_factual_relation_new(r->identifier,
|
||||||
|
r->person_identifier,r->evidence_identifier,r->ocr_run_identifier,
|
||||||
|
r->relation_type,r->factual_note,r->observed_at,r->active):NULL;}
|
||||||
|
void person_evidence_factual_relation_free(PersonEvidenceFactualRelation*r)
|
||||||
|
{if(!r)return;g_free(r->identifier);g_free(r->person_identifier);
|
||||||
|
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);}
|
||||||
|
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(
|
||||||
|
IdentificationStatusVocabularyEntry*e)
|
||||||
|
{person_role_vocabulary_entry_free(e);}
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
#include "views/create_person_dialog.h"
|
#include "views/create_person_dialog.h"
|
||||||
#include "views/dialog_geometry.h"
|
#include "views/dialog_geometry.h"
|
||||||
|
#include "views/person_vocabulary_adapter.h"
|
||||||
|
#include "views/identity_ocr_option_adapter.h"
|
||||||
#include "core/evidence_staging.h"
|
#include "core/evidence_staging.h"
|
||||||
#include "core/evidence_staging_task.h"
|
#include "core/evidence_staging_task.h"
|
||||||
#include "core/person_confirmation_summary.h"
|
#include "core/person_confirmation_summary.h"
|
||||||
|
|
@ -78,7 +80,8 @@ typedef struct
|
||||||
GtkStringList *evidence_labels;
|
GtkStringList *evidence_labels;
|
||||||
GtkStringList *retained_labels;
|
GtkStringList *retained_labels;
|
||||||
GtkStringList *type_filter_labels;
|
GtkStringList *type_filter_labels;
|
||||||
GtkCheckButton *roles[9];
|
GPtrArray *role_buttons;
|
||||||
|
PersonVocabularyAdapter *vocabularies;
|
||||||
guint step;
|
guint step;
|
||||||
GPtrArray *evidence_identifiers;
|
GPtrArray *evidence_identifiers;
|
||||||
GPtrArray *visible_records;
|
GPtrArray *visible_records;
|
||||||
|
|
@ -125,7 +128,6 @@ typedef struct {
|
||||||
GWeakRef window;
|
GWeakRef window;
|
||||||
guint64 generation;
|
guint64 generation;
|
||||||
} CreatePersonStagingContext;
|
} CreatePersonStagingContext;
|
||||||
static const char *const status_codes[] = {"unknown", "suspected", "confirmed"};
|
|
||||||
static void create_person_dialog_clear_preview(CreatePersonDialogState *state);
|
static void create_person_dialog_clear_preview(CreatePersonDialogState *state);
|
||||||
static void create_person_dialog_select_record(CreatePersonDialogState *state,
|
static void create_person_dialog_select_record(CreatePersonDialogState *state,
|
||||||
const EvidenceRecord *record, const char *business_type);
|
const EvidenceRecord *record, const char *business_type);
|
||||||
|
|
@ -162,33 +164,6 @@ static void identity_languages_worker(GTask *task, gpointer source,
|
||||||
else g_task_return_error(task, error);
|
else g_task_return_error(task, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *identity_language_label(const char *code)
|
|
||||||
{
|
|
||||||
if (g_str_equal(code, "fra")) return "Français (fra)";
|
|
||||||
if (g_str_equal(code, "eng")) return "Anglais (eng)";
|
|
||||||
if (g_str_equal(code, "fra+eng")) return "Français + anglais (fra+eng)";
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
static guint identity_default_language_index(const GPtrArray *codes)
|
|
||||||
{
|
|
||||||
guint english_index = GTK_INVALID_LIST_POSITION;
|
|
||||||
|
|
||||||
for (guint index = 0; codes != NULL && index < codes->len; index++) {
|
|
||||||
const char *code = g_ptr_array_index((GPtrArray *) codes, index);
|
|
||||||
|
|
||||||
if (g_strcmp0(code, "fra") == 0)
|
|
||||||
return index;
|
|
||||||
if (g_strcmp0(code, "eng") == 0)
|
|
||||||
english_index = index;
|
|
||||||
}
|
|
||||||
return english_index != GTK_INVALID_LIST_POSITION
|
|
||||||
? english_index
|
|
||||||
: (codes != NULL && codes->len > 0
|
|
||||||
? 0
|
|
||||||
: GTK_INVALID_LIST_POSITION);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char *create_person_dialog_effective_ocr_mime(
|
static const char *create_person_dialog_effective_ocr_mime(
|
||||||
const PersonEvidenceSelectionItem *item)
|
const PersonEvidenceSelectionItem *item)
|
||||||
{
|
{
|
||||||
|
|
@ -235,11 +210,13 @@ static void identity_languages_completed(GObject *source,
|
||||||
for (guint i = 0; state->ocr_language_codes != NULL &&
|
for (guint i = 0; state->ocr_language_codes != NULL &&
|
||||||
i < state->ocr_language_codes->len; i++) {
|
i < state->ocr_language_codes->len; i++) {
|
||||||
const char *code = g_ptr_array_index(state->ocr_language_codes, i);
|
const char *code = g_ptr_array_index(state->ocr_language_codes, i);
|
||||||
gtk_string_list_append(labels, identity_language_label(code));
|
gtk_string_list_append(labels,
|
||||||
|
identity_ocr_option_adapter_language_label(code));
|
||||||
}
|
}
|
||||||
gtk_drop_down_set_model(state->ocr_languages, G_LIST_MODEL(labels));
|
gtk_drop_down_set_model(state->ocr_languages, G_LIST_MODEL(labels));
|
||||||
gtk_drop_down_set_selected(state->ocr_languages,
|
gtk_drop_down_set_selected(state->ocr_languages,
|
||||||
identity_default_language_index(state->ocr_language_codes));
|
identity_ocr_option_adapter_default_language_index(
|
||||||
|
state->ocr_language_codes));
|
||||||
gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start),
|
gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start),
|
||||||
state->ocr_language_codes != NULL &&
|
state->ocr_language_codes != NULL &&
|
||||||
state->ocr_language_codes->len > 0);
|
state->ocr_language_codes->len > 0);
|
||||||
|
|
@ -749,6 +726,8 @@ static void create_person_dialog_state_free(gpointer data)
|
||||||
g_clear_pointer(&state->ocr_runs, g_ptr_array_unref);
|
g_clear_pointer(&state->ocr_runs, g_ptr_array_unref);
|
||||||
g_clear_pointer(&state->ocr_language_codes, g_ptr_array_unref);
|
g_clear_pointer(&state->ocr_language_codes, g_ptr_array_unref);
|
||||||
g_clear_pointer(&state->ocr_overlay, ocr_provenance_overlay_free);
|
g_clear_pointer(&state->ocr_overlay, ocr_provenance_overlay_free);
|
||||||
|
g_clear_pointer(&state->role_buttons, g_ptr_array_unref);
|
||||||
|
g_clear_pointer(&state->vocabularies, person_vocabulary_adapter_free);
|
||||||
person_dialog_lifecycle_cancel(state->lifecycle);
|
person_dialog_lifecycle_cancel(state->lifecycle);
|
||||||
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
|
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
|
||||||
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
|
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
|
||||||
|
|
@ -1301,6 +1280,8 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
||||||
CreatePersonDialogResult *result = NULL;
|
CreatePersonDialogResult *result = NULL;
|
||||||
char *notes = NULL;
|
char *notes = NULL;
|
||||||
guint status = gtk_drop_down_get_selected(state->status);
|
guint status = gtk_drop_down_get_selected(state->status);
|
||||||
|
const GPtrArray *statuses =
|
||||||
|
person_vocabulary_adapter_get_statuses(state->vocabularies);
|
||||||
(void) button;
|
(void) button;
|
||||||
if (state->session_check != NULL &&
|
if (state->session_check != NULL &&
|
||||||
!state->session_check(state->user_data)) {
|
!state->session_check(state->user_data)) {
|
||||||
|
|
@ -1309,7 +1290,7 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
||||||
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status >= G_N_ELEMENTS(status_codes) ||
|
if (statuses == NULL || status >= statuses->len ||
|
||||||
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0')
|
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0')
|
||||||
{
|
{
|
||||||
gtk_label_set_text(state->error, "La désignation de la personne est obligatoire.");
|
gtk_label_set_text(state->error, "La désignation de la personne est obligatoire.");
|
||||||
|
|
@ -1323,7 +1304,8 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
||||||
gtk_editable_get_text(GTK_EDITABLE(state->name)));
|
gtk_editable_get_text(GTK_EDITABLE(state->name)));
|
||||||
result->pseudonym = create_person_dialog_copy(
|
result->pseudonym = create_person_dialog_copy(
|
||||||
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)));
|
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)));
|
||||||
result->status = g_strdup(status_codes[status]);
|
result->status = g_strdup(person_vocabulary_adapter_status_code(
|
||||||
|
state->vocabularies, status));
|
||||||
result->notes = create_person_dialog_copy(notes);
|
result->notes = create_person_dialog_copy(notes);
|
||||||
if (person_evidence_selection_get_count(
|
if (person_evidence_selection_get_count(
|
||||||
state->person_evidence_selection) > 0) {
|
state->person_evidence_selection) > 0) {
|
||||||
|
|
@ -1340,23 +1322,10 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
||||||
result->input.notes = result->notes;
|
result->input.notes = result->notes;
|
||||||
result->input.confidence = gtk_spin_button_get_value_as_int(state->confidence);
|
result->input.confidence = gtk_spin_button_get_value_as_int(state->confidence);
|
||||||
result->input.evidence_identifier = result->evidence_identifier;
|
result->input.evidence_identifier = result->evidence_identifier;
|
||||||
result->role_assignments = g_ptr_array_new_with_free_func(
|
result->role_assignments =
|
||||||
(GDestroyNotify) person_role_assignment_input_free);
|
person_vocabulary_adapter_build_role_assignments(
|
||||||
static const char *const role_codes[] = {
|
state->vocabularies, state->role_buttons,
|
||||||
"alleged_author", "presented_identity",
|
result->evidence_identifier);
|
||||||
"potentially_impersonated_identity", "victim", "witness",
|
|
||||||
"declared_bank_holder", "intermediary", "mentioned_person", "other"};
|
|
||||||
for (guint i = 0; i < G_N_ELEMENTS(role_codes); i++)
|
|
||||||
if (gtk_check_button_get_active(state->roles[i])) {
|
|
||||||
PersonRoleAssignmentInput assignment = {
|
|
||||||
.role_code = (char *) role_codes[i],
|
|
||||||
.evidence_identifier = result->evidence_identifier,
|
|
||||||
.provenance_kind = "manual",
|
|
||||||
.has_confidence = FALSE
|
|
||||||
};
|
|
||||||
g_ptr_array_add(result->role_assignments,
|
|
||||||
person_role_assignment_input_copy(&assignment));
|
|
||||||
}
|
|
||||||
result->input.role_assignments = result->role_assignments;
|
result->input.role_assignments = result->role_assignments;
|
||||||
result->evidence_selection = state->person_evidence_selection;
|
result->evidence_selection = state->person_evidence_selection;
|
||||||
state->person_evidence_selection = NULL;
|
state->person_evidence_selection = NULL;
|
||||||
|
|
@ -1395,23 +1364,20 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
|
||||||
gtk_label_set_markup(state->progress, progress->str);
|
gtk_label_set_markup(state->progress, progress->str);
|
||||||
g_string_free(progress, TRUE);
|
g_string_free(progress, TRUE);
|
||||||
if (state->step == 4) {
|
if (state->step == 4) {
|
||||||
static const char *const status_labels[] = {
|
GPtrArray *role_labels =
|
||||||
"Inconnu", "Présumé", "Confirmé"};
|
person_vocabulary_adapter_selected_role_labels(
|
||||||
GPtrArray *role_labels = g_ptr_array_new();
|
state->role_buttons);
|
||||||
char *notes = create_person_dialog_get_notes(state);
|
char *notes = create_person_dialog_get_notes(state);
|
||||||
char *text;
|
char *text;
|
||||||
for (guint i = 0; i < G_N_ELEMENTS(state->roles); i++)
|
guint selected_status = gtk_drop_down_get_selected(state->status);
|
||||||
if (gtk_check_button_get_active(state->roles[i]))
|
const char *status_label =
|
||||||
g_ptr_array_add(role_labels, (gpointer)
|
person_vocabulary_adapter_status_label(
|
||||||
gtk_check_button_get_label(state->roles[i]));
|
state->vocabularies, selected_status);
|
||||||
text = person_confirmation_summary_build_multiple(
|
text = person_confirmation_summary_build_multiple(
|
||||||
gtk_editable_get_text(GTK_EDITABLE(state->designation)),
|
gtk_editable_get_text(GTK_EDITABLE(state->designation)),
|
||||||
gtk_editable_get_text(GTK_EDITABLE(state->name)),
|
gtk_editable_get_text(GTK_EDITABLE(state->name)),
|
||||||
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)),
|
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)),
|
||||||
gtk_drop_down_get_selected(state->status) <
|
status_label,
|
||||||
G_N_ELEMENTS(status_labels)
|
|
||||||
? status_labels[gtk_drop_down_get_selected(state->status)]
|
|
||||||
: "Non renseigné",
|
|
||||||
gtk_spin_button_get_value_as_int(state->confidence), notes,
|
gtk_spin_button_get_value_as_int(state->confidence), notes,
|
||||||
role_labels, state->person_evidence_selection);
|
role_labels, state->person_evidence_selection);
|
||||||
GString *confirmation = g_string_new(text);
|
GString *confirmation = g_string_new(text);
|
||||||
|
|
@ -1512,20 +1478,29 @@ static void create_person_dialog_add_row(GtkGrid *grid, int row,
|
||||||
gtk_grid_attach(grid, widget, 1, row, 1, 1);
|
gtk_grid_attach(grid, widget, 1, row, 1, 1);
|
||||||
}
|
}
|
||||||
gboolean create_person_dialog_present(GtkWindow *parent,
|
gboolean create_person_dialog_present(GtkWindow *parent,
|
||||||
const GPtrArray *records, const char *investigation_root_path,
|
Database *database, const GPtrArray *records,
|
||||||
|
const char *investigation_root_path,
|
||||||
TaskManager *task_manager, const ToolInfo *tesseract_tool,
|
TaskManager *task_manager, const ToolInfo *tesseract_tool,
|
||||||
CreatePersonDialogSessionCheck session_check,
|
CreatePersonDialogSessionCheck session_check,
|
||||||
CreatePersonDialogCallback callback, gpointer data,
|
CreatePersonDialogCallback callback, gpointer data,
|
||||||
GDestroyNotify data_destroy)
|
GDestroyNotify data_destroy)
|
||||||
{
|
{
|
||||||
static const char *const statuses[] = {"Inconnu", "Présumé", "Confirmé", NULL};
|
|
||||||
CreatePersonDialogState *state = NULL;
|
CreatePersonDialogState *state = NULL;
|
||||||
GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL;
|
GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL;
|
||||||
GtkWidget *roles_box = NULL, *evidence_box = NULL, *ocr_box = NULL;
|
GtkWidget *roles_box = NULL, *evidence_box = NULL, *ocr_box = NULL;
|
||||||
GtkWidget *summary = NULL;
|
GtkWidget *summary = NULL;
|
||||||
if (parent == NULL || investigation_root_path == NULL ||
|
if (parent == NULL || database == NULL ||
|
||||||
|
investigation_root_path == NULL ||
|
||||||
task_manager == NULL) return FALSE;
|
task_manager == NULL) return FALSE;
|
||||||
state = g_new0(CreatePersonDialogState, 1);
|
state = g_new0(CreatePersonDialogState, 1);
|
||||||
|
GError *vocabulary_error = NULL;
|
||||||
|
state->vocabularies = person_vocabulary_adapter_new(
|
||||||
|
database, &vocabulary_error);
|
||||||
|
if (state->vocabularies == NULL) {
|
||||||
|
g_clear_error(&vocabulary_error);
|
||||||
|
g_free(state);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
state->callback = callback; state->session_check = session_check;
|
state->callback = callback; state->session_check = session_check;
|
||||||
state->user_data = data; state->user_data_destroy = data_destroy;
|
state->user_data = data; state->user_data_destroy = data_destroy;
|
||||||
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
|
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
|
||||||
|
|
@ -1568,7 +1543,10 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
||||||
gtk_entry_set_placeholder_text(state->designation, "Personne présumée liée aux comptes");
|
gtk_entry_set_placeholder_text(state->designation, "Personne présumée liée aux comptes");
|
||||||
state->name = GTK_ENTRY(gtk_entry_new());
|
state->name = GTK_ENTRY(gtk_entry_new());
|
||||||
state->pseudonym = GTK_ENTRY(gtk_entry_new());
|
state->pseudonym = GTK_ENTRY(gtk_entry_new());
|
||||||
state->status = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(statuses));
|
GtkStringList *status_labels =
|
||||||
|
person_vocabulary_adapter_create_status_labels(state->vocabularies);
|
||||||
|
state->status = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||||
|
G_LIST_MODEL(status_labels), NULL));
|
||||||
gtk_drop_down_set_selected(state->status, 1);
|
gtk_drop_down_set_selected(state->status, 1);
|
||||||
state->confidence = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(0, 100, 5));
|
state->confidence = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(0, 100, 5));
|
||||||
gtk_spin_button_set_value(state->confidence, 30);
|
gtk_spin_button_set_value(state->confidence, 30);
|
||||||
|
|
@ -1620,15 +1598,9 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
||||||
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
|
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
|
||||||
gtk_stack_add_titled(state->stack, grid, "person", "1 — Personne");
|
gtk_stack_add_titled(state->stack, grid, "person", "1 — Personne");
|
||||||
roles_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
roles_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||||
static const char *const role_codes[] = {
|
state->role_buttons =
|
||||||
"alleged_author", "presented_identity",
|
person_vocabulary_adapter_create_role_buttons(
|
||||||
"potentially_impersonated_identity", "victim", "witness",
|
state->vocabularies, GTK_BOX(roles_box));
|
||||||
"declared_bank_holder", "intermediary", "mentioned_person", "other"};
|
|
||||||
for (guint i = 0; i < G_N_ELEMENTS(role_codes); i++) {
|
|
||||||
state->roles[i] = GTK_CHECK_BUTTON(gtk_check_button_new_with_label(
|
|
||||||
person_role_assignment_role_label(role_codes[i])));
|
|
||||||
gtk_box_append(GTK_BOX(roles_box), GTK_WIDGET(state->roles[i]));
|
|
||||||
}
|
|
||||||
gtk_stack_add_titled(state->stack, roles_box, "roles", "2 — Rôles");
|
gtk_stack_add_titled(state->stack, roles_box, "roles", "2 — Rôles");
|
||||||
evidence_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
evidence_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
gtk_box_append(GTK_BOX(evidence_box), gtk_label_new(
|
gtk_box_append(GTK_BOX(evidence_box), gtk_label_new(
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,8 @@ static void paned_geometry_apply(
|
||||||
{
|
{
|
||||||
int width = gtk_widget_get_width(GTK_WIDGET(paned));
|
int width = gtk_widget_get_width(GTK_WIDGET(paned));
|
||||||
int position;
|
int position;
|
||||||
if (width <= 0 || geometry == NULL) return;
|
if (geometry == NULL ||
|
||||||
|
width < geometry->minimum_start + geometry->minimum_end) return;
|
||||||
position = (int) (width * geometry->ratio);
|
position = (int) (width * geometry->ratio);
|
||||||
position = MAX(geometry->minimum_start, position);
|
position = MAX(geometry->minimum_start, position);
|
||||||
position = MIN(position, MAX(0, width - geometry->minimum_end));
|
position = MIN(position, MAX(0, width - geometry->minimum_end));
|
||||||
|
|
@ -120,7 +121,8 @@ static gboolean paned_geometry_idle(gpointer data)
|
||||||
LabfyPanedGeometry *geometry = g_object_get_data(
|
LabfyPanedGeometry *geometry = g_object_get_data(
|
||||||
G_OBJECT(paned), "labfy-paned-initial-geometry");
|
G_OBJECT(paned), "labfy-paned-initial-geometry");
|
||||||
if (geometry == NULL) return G_SOURCE_REMOVE;
|
if (geometry == NULL) return G_SOURCE_REMOVE;
|
||||||
if (gtk_widget_get_width(GTK_WIDGET(paned)) <= 0)
|
if (gtk_widget_get_width(GTK_WIDGET(paned)) <
|
||||||
|
geometry->minimum_start + geometry->minimum_end)
|
||||||
return G_SOURCE_CONTINUE;
|
return G_SOURCE_CONTINUE;
|
||||||
paned_geometry_apply(paned, geometry);
|
paned_geometry_apply(paned, geometry);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
|
|
|
||||||
23
src/views/identity_ocr_option_adapter.c
Normal file
23
src/views/identity_ocr_option_adapter.c
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#include "views/identity_ocr_option_adapter.h"
|
||||||
|
|
||||||
|
const char *identity_ocr_option_adapter_language_label(const char *code)
|
||||||
|
{
|
||||||
|
if (g_str_equal(code, "fra")) return "Français (fra)";
|
||||||
|
if (g_str_equal(code, "eng")) return "Anglais (eng)";
|
||||||
|
if (g_str_equal(code, "fra+eng"))
|
||||||
|
return "Français + anglais (fra+eng)";
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
guint identity_ocr_option_adapter_default_language_index(
|
||||||
|
const GPtrArray *codes)
|
||||||
|
{
|
||||||
|
guint english_index = G_MAXUINT;
|
||||||
|
for (guint index = 0; codes != NULL && index < codes->len; index++) {
|
||||||
|
const char *code = g_ptr_array_index((GPtrArray *) codes, index);
|
||||||
|
if (g_strcmp0(code, "fra") == 0) return index;
|
||||||
|
if (g_strcmp0(code, "eng") == 0) english_index = index;
|
||||||
|
}
|
||||||
|
return english_index != G_MAXUINT ? english_index :
|
||||||
|
(codes != NULL && codes->len > 0 ? 0 : G_MAXUINT);
|
||||||
|
}
|
||||||
154
src/views/person_vocabulary_adapter.c
Normal file
154
src/views/person_vocabulary_adapter.c
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
#include "views/person_vocabulary_adapter.h"
|
||||||
|
#include "dao/identity_traceability_dao.h"
|
||||||
|
#include "models/person_role_assignment.h"
|
||||||
|
|
||||||
|
struct PersonVocabularyAdapter {
|
||||||
|
GPtrArray *roles;
|
||||||
|
GPtrArray *statuses;
|
||||||
|
};
|
||||||
|
|
||||||
|
PersonVocabularyAdapter *person_vocabulary_adapter_new(
|
||||||
|
Database *database, GError **error)
|
||||||
|
{
|
||||||
|
IdentityTraceabilityDao *dao = identity_traceability_dao_new(database);
|
||||||
|
PersonVocabularyAdapter *adapter = NULL;
|
||||||
|
if (dao == NULL) return NULL;
|
||||||
|
adapter = g_new0(PersonVocabularyAdapter, 1);
|
||||||
|
adapter->roles = identity_traceability_dao_list_roles(
|
||||||
|
dao, FALSE, error);
|
||||||
|
if (adapter->roles != NULL)
|
||||||
|
adapter->statuses =
|
||||||
|
identity_traceability_dao_list_identification_statuses(
|
||||||
|
dao, FALSE, error);
|
||||||
|
identity_traceability_dao_free(dao);
|
||||||
|
if (adapter->roles == NULL || adapter->statuses == NULL) {
|
||||||
|
person_vocabulary_adapter_free(adapter);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void person_vocabulary_adapter_free(PersonVocabularyAdapter *adapter)
|
||||||
|
{
|
||||||
|
if (adapter == NULL) return;
|
||||||
|
g_clear_pointer(&adapter->roles, g_ptr_array_unref);
|
||||||
|
g_clear_pointer(&adapter->statuses, g_ptr_array_unref);
|
||||||
|
g_free(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GPtrArray *person_vocabulary_adapter_get_roles(
|
||||||
|
const PersonVocabularyAdapter *adapter)
|
||||||
|
{ return adapter != NULL ? adapter->roles : NULL; }
|
||||||
|
|
||||||
|
const GPtrArray *person_vocabulary_adapter_get_statuses(
|
||||||
|
const PersonVocabularyAdapter *adapter)
|
||||||
|
{ return adapter != NULL ? adapter->statuses : NULL; }
|
||||||
|
|
||||||
|
static GtkStringList *create_labels(const GPtrArray *entries)
|
||||||
|
{
|
||||||
|
GtkStringList *labels = gtk_string_list_new(NULL);
|
||||||
|
for (guint i = 0; entries != NULL && i < entries->len; i++) {
|
||||||
|
PersonRoleVocabularyEntry *entry = g_ptr_array_index(
|
||||||
|
(GPtrArray *) entries, i);
|
||||||
|
gtk_string_list_append(labels, entry->label);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkStringList *person_vocabulary_adapter_create_role_labels(
|
||||||
|
const PersonVocabularyAdapter *adapter)
|
||||||
|
{ return create_labels(person_vocabulary_adapter_get_roles(adapter)); }
|
||||||
|
|
||||||
|
GtkStringList *person_vocabulary_adapter_create_status_labels(
|
||||||
|
const PersonVocabularyAdapter *adapter)
|
||||||
|
{ return create_labels(person_vocabulary_adapter_get_statuses(adapter)); }
|
||||||
|
|
||||||
|
const char *person_vocabulary_adapter_status_code(
|
||||||
|
const PersonVocabularyAdapter *adapter, guint index)
|
||||||
|
{
|
||||||
|
const GPtrArray *statuses =
|
||||||
|
person_vocabulary_adapter_get_statuses(adapter);
|
||||||
|
return statuses != NULL && index < statuses->len
|
||||||
|
? ((IdentificationStatusVocabularyEntry *) g_ptr_array_index(
|
||||||
|
(GPtrArray *) statuses, index))->code : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *person_vocabulary_adapter_status_label(
|
||||||
|
const PersonVocabularyAdapter *adapter, guint index)
|
||||||
|
{
|
||||||
|
const GPtrArray *statuses =
|
||||||
|
person_vocabulary_adapter_get_statuses(adapter);
|
||||||
|
return statuses != NULL && index < statuses->len
|
||||||
|
? ((IdentificationStatusVocabularyEntry *) g_ptr_array_index(
|
||||||
|
(GPtrArray *) statuses, index))->label : "Non renseigné";
|
||||||
|
}
|
||||||
|
|
||||||
|
GPtrArray *person_vocabulary_adapter_selected_role_labels(
|
||||||
|
const GPtrArray *buttons)
|
||||||
|
{
|
||||||
|
GPtrArray *labels = g_ptr_array_new();
|
||||||
|
for (guint i = 0; buttons != NULL && i < buttons->len; i++) {
|
||||||
|
GtkCheckButton *button = g_ptr_array_index(
|
||||||
|
(GPtrArray *) buttons, i);
|
||||||
|
if (gtk_check_button_get_active(button))
|
||||||
|
g_ptr_array_add(labels, (gpointer)
|
||||||
|
gtk_check_button_get_label(button));
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
GPtrArray *person_vocabulary_adapter_create_role_buttons(
|
||||||
|
const PersonVocabularyAdapter *adapter, GtkBox *container)
|
||||||
|
{
|
||||||
|
const GPtrArray *roles = person_vocabulary_adapter_get_roles(adapter);
|
||||||
|
GPtrArray *buttons = g_ptr_array_new();
|
||||||
|
for (guint i = 0; roles != NULL && i < roles->len; i++) {
|
||||||
|
PersonRoleVocabularyEntry *entry =
|
||||||
|
g_ptr_array_index((GPtrArray *) roles, i);
|
||||||
|
GtkCheckButton *button = GTK_CHECK_BUTTON(
|
||||||
|
gtk_check_button_new_with_label(entry->label));
|
||||||
|
gtk_widget_set_tooltip_text(GTK_WIDGET(button), entry->description);
|
||||||
|
g_ptr_array_add(buttons, button);
|
||||||
|
gtk_box_append(container, GTK_WIDGET(button));
|
||||||
|
}
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
GPtrArray *person_vocabulary_adapter_build_role_assignments(
|
||||||
|
const PersonVocabularyAdapter *adapter, const GPtrArray *buttons,
|
||||||
|
const char *evidence_identifier)
|
||||||
|
{
|
||||||
|
const GPtrArray *roles = person_vocabulary_adapter_get_roles(adapter);
|
||||||
|
GPtrArray *assignments = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) person_role_assignment_input_free);
|
||||||
|
for (guint i = 0; roles != NULL && buttons != NULL &&
|
||||||
|
i < roles->len && i < buttons->len; i++) {
|
||||||
|
if (!gtk_check_button_get_active(g_ptr_array_index(
|
||||||
|
(GPtrArray *) buttons, i))) continue;
|
||||||
|
PersonRoleVocabularyEntry *entry =
|
||||||
|
g_ptr_array_index((GPtrArray *) roles, i);
|
||||||
|
PersonRoleAssignmentInput input = {
|
||||||
|
.role_code = entry->code,
|
||||||
|
.evidence_identifier = (char *) evidence_identifier,
|
||||||
|
.provenance_kind = "manual"
|
||||||
|
};
|
||||||
|
g_ptr_array_add(assignments,
|
||||||
|
person_role_assignment_input_copy(&input));
|
||||||
|
}
|
||||||
|
return assignments;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean person_vocabulary_adapter_justification_valid(
|
||||||
|
const PersonRoleVocabularyEntry *entry, const char *justification)
|
||||||
|
{
|
||||||
|
char *copy;
|
||||||
|
gboolean valid;
|
||||||
|
if (entry == NULL) return FALSE;
|
||||||
|
if (!entry->requires_justification) return TRUE;
|
||||||
|
copy = g_strdup(justification);
|
||||||
|
if (copy != NULL) g_strstrip(copy);
|
||||||
|
valid = copy != NULL && copy[0] != '\0' &&
|
||||||
|
g_utf8_validate(copy, -1, NULL);
|
||||||
|
g_free(copy);
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -1,6 +1,8 @@
|
||||||
#include "views/create_person_dialog.h"
|
#include "views/create_person_dialog.h"
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|
@ -241,6 +243,16 @@ static void activate(GtkApplication *application, gpointer data)
|
||||||
{
|
{
|
||||||
TestContext *context = data;
|
TestContext *context = data;
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
|
char *database_path = NULL;
|
||||||
|
int database_fd = g_file_open_tmp(
|
||||||
|
"labfy-create-person-XXXXXX.sqlite", &database_path, &error);
|
||||||
|
g_assert_cmpint(database_fd, >=, 0);
|
||||||
|
close(database_fd);
|
||||||
|
g_unlink(database_path);
|
||||||
|
g_assert_true(database_initialize(database_path,
|
||||||
|
"Enquête SPECIMEN dialogue", "/tmp"));
|
||||||
|
Database *database = database_open(database_path);
|
||||||
|
g_assert_nonnull(database);
|
||||||
GPtrArray *records = g_ptr_array_new_with_free_func(
|
GPtrArray *records = g_ptr_array_new_with_free_func(
|
||||||
(GDestroyNotify) evidence_record_free);
|
(GDestroyNotify) evidence_record_free);
|
||||||
EvidenceRecord *record = evidence_record_new(
|
EvidenceRecord *record = evidence_record_new(
|
||||||
|
|
@ -270,8 +282,11 @@ static void activate(GtkApplication *application, gpointer data)
|
||||||
gtk_application_window_new(application));
|
gtk_application_window_new(application));
|
||||||
gtk_window_present(context->main_window);
|
gtk_window_present(context->main_window);
|
||||||
g_assert_true(create_person_dialog_present(context->main_window,
|
g_assert_true(create_person_dialog_present(context->main_window,
|
||||||
records, "/tmp", context->task_manager, NULL, NULL, completed,
|
database, records, "/tmp", context->task_manager, NULL, NULL, completed,
|
||||||
context, NULL));
|
context, NULL));
|
||||||
|
database_close(database);
|
||||||
|
g_unlink(database_path);
|
||||||
|
g_free(database_path);
|
||||||
g_ptr_array_unref(records);
|
g_ptr_array_unref(records);
|
||||||
g_idle_add(click_cancel, context);
|
g_idle_add(click_cancel, context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,10 @@ static gboolean drive(gpointer data)
|
||||||
g_assert_true(gtk_widget_get_mapped(actions));
|
g_assert_true(gtk_widget_get_mapped(actions));
|
||||||
if(context->layout_index==0){
|
if(context->layout_index==0){
|
||||||
int position=gtk_paned_get_position(GTK_PANED(paned));
|
int position=gtk_paned_get_position(GTK_PANED(paned));
|
||||||
g_assert_cmpint(position,>=,600);
|
int allocated=gtk_widget_get_width(paned);
|
||||||
g_assert_cmpint(position,<=,680);
|
g_assert_cmpint(position,>=,480);
|
||||||
|
g_assert_cmpint(allocated-position,>=,240);
|
||||||
|
g_assert_cmpint(position,<,allocated-allocated/10);
|
||||||
}
|
}
|
||||||
context->layout_index++;
|
context->layout_index++;
|
||||||
context->layout_resize_pending=FALSE;
|
context->layout_resize_pending=FALSE;
|
||||||
|
|
@ -582,10 +584,12 @@ static gboolean drive(gpointer data)
|
||||||
verify_persisted_review(context);
|
verify_persisted_review(context);
|
||||||
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(context->main_window)));
|
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(context->main_window)));
|
||||||
GPtrArray *empty=g_ptr_array_new();
|
GPtrArray *empty=g_ptr_array_new();
|
||||||
g_assert_true(create_person_dialog_present(context->main_window,empty,
|
Database *database = database_open(context->database_path);
|
||||||
|
g_assert_true(create_person_dialog_present(context->main_window,database,empty,
|
||||||
context->root,context->task_manager,
|
context->root,context->task_manager,
|
||||||
tool_registry_find(context->registry,"tesseract"),NULL,completed,
|
tool_registry_find(context->registry,"tesseract"),NULL,completed,
|
||||||
context,NULL));
|
context,NULL));
|
||||||
|
database_close(database);
|
||||||
g_ptr_array_unref(empty);
|
g_ptr_array_unref(empty);
|
||||||
GList *windows=gtk_application_get_windows(context->application);
|
GList *windows=gtk_application_get_windows(context->application);
|
||||||
context->dialog=windows->data==context->main_window
|
context->dialog=windows->data==context->main_window
|
||||||
|
|
@ -667,7 +671,7 @@ static void activate(GtkApplication *application,gpointer data)
|
||||||
g_assert_true(evidence_dao_insert(evidence_dao,
|
g_assert_true(evidence_dao_insert(evidence_dao,
|
||||||
g_ptr_array_index(records,index),&error));
|
g_ptr_array_index(records,index),&error));
|
||||||
g_assert_no_error(error);
|
g_assert_no_error(error);
|
||||||
evidence_dao_free(evidence_dao);database_close(database);
|
evidence_dao_free(evidence_dao);
|
||||||
context->registry=tool_registry_new();
|
context->registry=tool_registry_new();
|
||||||
char *tool=g_canonicalize_filename("tests/fake_document_tool",NULL);
|
char *tool=g_canonicalize_filename("tests/fake_document_tool",NULL);
|
||||||
g_assert_true(tool_registry_register(context->registry,"tesseract",
|
g_assert_true(tool_registry_register(context->registry,"tesseract",
|
||||||
|
|
@ -677,10 +681,11 @@ static void activate(GtkApplication *application,gpointer data)
|
||||||
"5.0.0 SPECIMEN",&error));g_assert_no_error(error);
|
"5.0.0 SPECIMEN",&error));g_assert_no_error(error);
|
||||||
context->main_window=GTK_WINDOW(gtk_application_window_new(application));
|
context->main_window=GTK_WINDOW(gtk_application_window_new(application));
|
||||||
gtk_window_present(context->main_window);
|
gtk_window_present(context->main_window);
|
||||||
g_assert_true(create_person_dialog_present(context->main_window,records,
|
g_assert_true(create_person_dialog_present(context->main_window,database,records,
|
||||||
context->root,context->task_manager,
|
context->root,context->task_manager,
|
||||||
tool_registry_find(context->registry,"tesseract"),NULL,completed,
|
tool_registry_find(context->registry,"tesseract"),NULL,completed,
|
||||||
context,NULL));
|
context,NULL));
|
||||||
|
database_close(database);
|
||||||
GList *windows=gtk_application_get_windows(application);
|
GList *windows=gtk_application_get_windows(application);
|
||||||
context->dialog=windows->data==context->main_window
|
context->dialog=windows->data==context->main_window
|
||||||
?GTK_WINDOW(windows->next->data):GTK_WINDOW(windows->data);
|
?GTK_WINDOW(windows->next->data):GTK_WINDOW(windows->data);
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
|
||||||
"FROM investigation;"
|
"FROM investigation;"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(strcmp(schema_version, "17") == 0);
|
assert(strcmp(schema_version, "18") == 0);
|
||||||
test_database_assert_table_exists(database, "person_role_assignments");
|
test_database_assert_table_exists(database, "person_role_assignments");
|
||||||
test_database_assert_table_exists(database, "bank_account_entities");
|
test_database_assert_table_exists(database, "bank_account_entities");
|
||||||
test_database_assert_table_exists(database, "relation_types");
|
test_database_assert_table_exists(database, "relation_types");
|
||||||
|
|
@ -535,6 +535,14 @@ static void test_database_initialize_valid_database(void)
|
||||||
test_database_assert_table_exists(database, "osint_execution_relations");
|
test_database_assert_table_exists(database, "osint_execution_relations");
|
||||||
test_database_assert_table_exists(database, "comptes_sociaux");
|
test_database_assert_table_exists(database, "comptes_sociaux");
|
||||||
test_database_assert_table_exists(database, "person_roles");
|
test_database_assert_table_exists(database, "person_roles");
|
||||||
|
test_database_assert_table_exists(database,
|
||||||
|
"document_authenticity_assessments");
|
||||||
|
test_database_assert_table_exists(database,
|
||||||
|
"person_evidence_factual_relations");
|
||||||
|
test_database_assert_table_exists(database,
|
||||||
|
"person_role_vocabulary");
|
||||||
|
test_database_assert_table_exists(database,
|
||||||
|
"identification_status_vocabulary");
|
||||||
assert(strcmp(application_name, "Labfy Investigation") == 0);
|
assert(strcmp(application_name, "Labfy Investigation") == 0);
|
||||||
|
|
||||||
assert(created_at[0] != '\0');
|
assert(created_at[0] != '\0');
|
||||||
|
|
@ -993,7 +1001,7 @@ static void test_database_migrate_v1_to_v2(void)
|
||||||
assert(
|
assert(
|
||||||
strcmp(
|
strcmp(
|
||||||
schema_version,
|
schema_version,
|
||||||
"17"
|
"18"
|
||||||
) == 0
|
) == 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1422,6 +1430,14 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
|
||||||
"INSERT INTO person_roles(entity_id,role,updated_at) VALUES("
|
"INSERT INTO person_roles(entity_id,role,updated_at) VALUES("
|
||||||
"'20000000-0000-4000-8000-000000000014','alleged_scammer',"
|
"'20000000-0000-4000-8000-000000000014','alleged_scammer',"
|
||||||
"'2026-07-28T08:00:00Z');"
|
"'2026-07-28T08:00:00Z');"
|
||||||
|
"DROP TABLE person_identification_assessments;"
|
||||||
|
"DROP TABLE person_evidence_factual_relations;"
|
||||||
|
"DROP TABLE document_authenticity_assessments;"
|
||||||
|
"DROP TABLE person_role_vocabulary;"
|
||||||
|
"DROP TABLE identification_status_vocabulary;"
|
||||||
|
"DROP TABLE identity_field_observations;"
|
||||||
|
"DROP TABLE identity_document_observations;"
|
||||||
|
"DROP TABLE identity_ocr_runs;"
|
||||||
"DROP TABLE preuve_entite_sources;"
|
"DROP TABLE preuve_entite_sources;"
|
||||||
"DROP TABLE person_role_assignments;"
|
"DROP TABLE person_role_assignments;"
|
||||||
"UPDATE metadata SET value='12' WHERE key='schema_version';"
|
"UPDATE metadata SET value='12' WHERE key='schema_version';"
|
||||||
|
|
@ -1448,7 +1464,7 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
|
||||||
legacy_sources = test_database_read_single_text(sqlite_database,
|
legacy_sources = test_database_read_single_text(sqlite_database,
|
||||||
"SELECT COUNT(*) FROM preuve_entite_sources "
|
"SELECT COUNT(*) FROM preuve_entite_sources "
|
||||||
"WHERE source_kind='legacy_manual';");
|
"WHERE source_kind='legacy_manual';");
|
||||||
assert(strcmp(version, "17") == 0);
|
assert(strcmp(version, "18") == 0);
|
||||||
assert(strcmp(legacy_sources, "1") == 0);
|
assert(strcmp(legacy_sources, "1") == 0);
|
||||||
char *legacy_role = test_database_read_single_text(sqlite_database,
|
char *legacy_role = test_database_read_single_text(sqlite_database,
|
||||||
"SELECT role_code || ':' || provenance_kind "
|
"SELECT role_code || ':' || provenance_kind "
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,20 @@ static void test_paned_applied_once(void)
|
||||||
gtk_window_present(window);
|
gtk_window_present(window);
|
||||||
for (guint frame = 0; frame < 6; frame++)
|
for (guint frame = 0; frame < 6; frame++)
|
||||||
wait_for_frame(GTK_WIDGET(window));
|
wait_for_frame(GTK_WIDGET(window));
|
||||||
int expected = (gtk_widget_get_width(GTK_WIDGET(paned)) * 2) / 3;
|
int allocated = gtk_widget_get_width(GTK_WIDGET(paned));
|
||||||
g_assert_cmpint(ABS(gtk_paned_get_position(paned) - expected), <=, 2);
|
int expected = (allocated * 2) / 3;
|
||||||
gtk_paned_set_position(paned, 450);
|
int tolerance = MAX(4, allocated / 50);
|
||||||
|
int position = gtk_paned_get_position(paned);
|
||||||
|
g_assert_cmpint(ABS(position - expected), <=, tolerance);
|
||||||
|
g_assert_cmpint(position, >=, 320);
|
||||||
|
g_assert_cmpint(allocated - position, >=, 180);
|
||||||
|
int user_position = (allocated * 11) / 20;
|
||||||
|
gtk_paned_set_position(paned, user_position);
|
||||||
gtk_window_set_default_size(window, 1000, 700);
|
gtk_window_set_default_size(window, 1000, 700);
|
||||||
wait_for_frame(GTK_WIDGET(window));
|
wait_for_frame(GTK_WIDGET(window));
|
||||||
g_assert_cmpint(gtk_paned_get_position(paned), ==, 450);
|
g_assert_cmpint(gtk_paned_get_position(paned), ==, user_position);
|
||||||
labfy_paned_apply_initial_ratio(paned, 0.5, 100, 100);
|
labfy_paned_apply_initial_ratio(paned, 0.5, 100, 100);
|
||||||
g_assert_cmpint(gtk_paned_get_position(paned), ==, 450);
|
g_assert_cmpint(gtk_paned_get_position(paned), ==, user_position);
|
||||||
gtk_window_destroy(window);
|
gtk_window_destroy(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ typedef struct {
|
||||||
gboolean import_done;
|
gboolean import_done;
|
||||||
gboolean cancellation_done;
|
gboolean cancellation_done;
|
||||||
gboolean layout_resize_pending;
|
gboolean layout_resize_pending;
|
||||||
|
int user_paned_position;
|
||||||
gboolean passed;
|
gboolean passed;
|
||||||
gboolean revision_done;
|
gboolean revision_done;
|
||||||
char *final_evidence_identifier;
|
char *final_evidence_identifier;
|
||||||
|
|
@ -501,19 +502,35 @@ static gboolean drive(gpointer data)
|
||||||
if (!context->layout_resize_pending) {
|
if (!context->layout_resize_pending) {
|
||||||
int initial_position =
|
int initial_position =
|
||||||
gtk_paned_get_position(GTK_PANED(paned));
|
gtk_paned_get_position(GTK_PANED(paned));
|
||||||
int expected_position =
|
int allocated_width = gtk_widget_get_width(paned);
|
||||||
(gtk_widget_get_width(paned) * 2) / 3;
|
int start_min = 0, end_min = 0;
|
||||||
|
gtk_widget_measure(gtk_paned_get_start_child(GTK_PANED(paned)),
|
||||||
|
GTK_ORIENTATION_HORIZONTAL, -1, &start_min, NULL, NULL, NULL);
|
||||||
|
gtk_widget_measure(gtk_paned_get_end_child(GTK_PANED(paned)),
|
||||||
|
GTK_ORIENTATION_HORIZONTAL, -1, &end_min, NULL, NULL, NULL);
|
||||||
|
int minimum_start = MAX(480, start_min);
|
||||||
|
int minimum_end = MAX(320, end_min);
|
||||||
|
int expected_position = CLAMP((allocated_width * 2) / 3,
|
||||||
|
minimum_start, MAX(minimum_start,
|
||||||
|
allocated_width - minimum_end));
|
||||||
|
int tolerance = MAX(4, allocated_width / 50);
|
||||||
g_assert_cmpint(
|
g_assert_cmpint(
|
||||||
ABS(initial_position - expected_position), <=, 3);
|
ABS(initial_position - expected_position), <=, tolerance);
|
||||||
gtk_paned_set_position(GTK_PANED(paned), 650);
|
g_assert_cmpint(initial_position, >=, minimum_start);
|
||||||
|
g_assert_cmpint(allocated_width - initial_position, >=,
|
||||||
|
minimum_end);
|
||||||
|
int user_position = (allocated_width * 11) / 20;
|
||||||
|
gtk_paned_set_position(GTK_PANED(paned), user_position);
|
||||||
g_assert_cmpint(
|
g_assert_cmpint(
|
||||||
gtk_paned_get_position(GTK_PANED(paned)), ==, 650);
|
gtk_paned_get_position(GTK_PANED(paned)), ==, user_position);
|
||||||
|
context->user_paned_position = user_position;
|
||||||
gtk_window_set_default_size(dialog, 1000, 700);
|
gtk_window_set_default_size(dialog, 1000, 700);
|
||||||
context->layout_resize_pending = TRUE;
|
context->layout_resize_pending = TRUE;
|
||||||
return G_SOURCE_CONTINUE;
|
return G_SOURCE_CONTINUE;
|
||||||
}
|
}
|
||||||
g_assert_cmpint(
|
g_assert_cmpint(
|
||||||
gtk_paned_get_position(GTK_PANED(paned)), ==, 650);
|
gtk_paned_get_position(GTK_PANED(paned)), ==,
|
||||||
|
context->user_paned_position);
|
||||||
GtkDropDown *person = GTK_DROP_DOWN(find_named(root,
|
GtkDropDown *person = GTK_DROP_DOWN(find_named(root,
|
||||||
"identity-person"));
|
"identity-person"));
|
||||||
g_assert_nonnull(person);
|
g_assert_nonnull(person);
|
||||||
|
|
|
||||||
233
tests/test_identity_traceability.c
Normal file
233
tests/test_identity_traceability.c
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
#include "dao/identity_traceability_dao.h"
|
||||||
|
#include "views/person_vocabulary_adapter.h"
|
||||||
|
#include "dao/identity_ocr_dao.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "models/identity_traceability.h"
|
||||||
|
#include <glib.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
#define EVIDENCE "10000000-0000-4000-8000-000000000018"
|
||||||
|
#define PERSON "20000000-0000-4000-8000-000000000018"
|
||||||
|
#define RUN "30000000-0000-4000-8000-000000000018"
|
||||||
|
#define DOCUMENT "40000000-0000-4000-8000-000000000018"
|
||||||
|
#define FIELD "50000000-0000-4000-8000-000000000018"
|
||||||
|
#define ASSESSMENT1 "60000000-0000-4000-8000-000000000018"
|
||||||
|
#define ASSESSMENT2 "60000000-0000-4000-8000-000000000019"
|
||||||
|
#define FACT "70000000-0000-4000-8000-000000000018"
|
||||||
|
#define AT "2026-07-30T10:00:00Z"
|
||||||
|
|
||||||
|
typedef struct{char*dir,*path;Database*database;}Fixture;
|
||||||
|
static void exec_ok(sqlite3*d,const char*sql)
|
||||||
|
{char*message=NULL;g_assert_cmpint(sqlite3_exec(d,sql,NULL,NULL,&message),==,SQLITE_OK);
|
||||||
|
sqlite3_free(message);}
|
||||||
|
static char *scalar(sqlite3*d,const char*sql)
|
||||||
|
{sqlite3_stmt*s=NULL;g_assert_cmpint(sqlite3_prepare_v2(d,sql,-1,&s,NULL),==,SQLITE_OK);
|
||||||
|
g_assert_cmpint(sqlite3_step(s),==,SQLITE_ROW);char*r=g_strdup(
|
||||||
|
(const char*)sqlite3_column_text(s,0));sqlite3_finalize(s);return r;}
|
||||||
|
static Fixture fixture_new(void)
|
||||||
|
{
|
||||||
|
Fixture f={0};GError*e=NULL;f.dir=g_dir_make_tmp("labfy-v18-XXXXXX",&e);
|
||||||
|
g_assert_no_error(e);f.path=g_build_filename(f.dir,"Enquete.sqlite",NULL);
|
||||||
|
g_assert_true(database_initialize(f.path,"SPECIMEN V18",f.dir));
|
||||||
|
sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
|
||||||
|
exec_ok(d,"PRAGMA foreign_keys=ON;"
|
||||||
|
"INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,"
|
||||||
|
"imported_at,updated_at,status,locked,original_name) VALUES('" EVIDENCE
|
||||||
|
"','specimen.png','specimen.png',2,8,"
|
||||||
|
"'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa','" AT
|
||||||
|
"','" AT "','active',0,'specimen.png');"
|
||||||
|
"INSERT INTO entites(id,type_id,valeur,label,confiance,created_at,updated_at,status)"
|
||||||
|
" VALUES('" PERSON "',(SELECT id FROM types_entite WHERE code='person'),"
|
||||||
|
"'Personne SPECIMEN','Personne SPECIMEN',0,'" AT "','" AT "','active');"
|
||||||
|
"INSERT INTO identity_ocr_runs(id,evidence_id,expected_sha256,page_number,"
|
||||||
|
"document_type,document_side,engine,requested_languages,available_languages,"
|
||||||
|
"parameters,preprocessing_profile,executed_at,status,text_relative_path,"
|
||||||
|
"text_sha256,tsv_relative_path,tsv_sha256) VALUES('" RUN "','" EVIDENCE "',"
|
||||||
|
"'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',1,"
|
||||||
|
"'identity_card','front','tesseract','fra','fra','SPECIMEN','none','" AT
|
||||||
|
"','success','raw.txt','bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',"
|
||||||
|
"'raw.tsv','cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc');"
|
||||||
|
"INSERT INTO identity_document_observations(id,person_id,evidence_id,ocr_run_id,"
|
||||||
|
"document_type,document_side,page_number,review_state,observed_at) VALUES('"
|
||||||
|
DOCUMENT "','" PERSON "','" EVIDENCE "','" RUN "','identity_card','front',1,"
|
||||||
|
"'accepted','" AT "');"
|
||||||
|
"INSERT INTO identity_field_observations(id,observation_id,field_code,raw_value,"
|
||||||
|
"normalized_value,review_status,origin,evidence_id,ocr_run_id,page_number,"
|
||||||
|
"display_order,reviewed_at) VALUES('" FIELD "','" DOCUMENT "','surname',"
|
||||||
|
"'BRUT SPECIMEN','BRUT SPECIMEN','accepted','ocr','" EVIDENCE "','" RUN
|
||||||
|
"',1,0,'" AT "');");
|
||||||
|
sqlite3_close(d);f.database=database_open(f.path);g_assert_nonnull(f.database);
|
||||||
|
g_assert_true(database_migrate_to_latest(f.database));return f;
|
||||||
|
}
|
||||||
|
static void fixture_free(Fixture*f)
|
||||||
|
{database_close(f->database);g_remove(f->path);g_rmdir(f->dir);
|
||||||
|
g_free(f->path);g_free(f->dir);}
|
||||||
|
|
||||||
|
static void test_models(void)
|
||||||
|
{
|
||||||
|
PersonRoleVocabularyEntry justified={.requires_justification=TRUE};
|
||||||
|
g_assert_false(person_vocabulary_adapter_justification_valid(
|
||||||
|
&justified," "));
|
||||||
|
g_assert_true(person_vocabulary_adapter_justification_valid(
|
||||||
|
&justified,"Justification SPECIMEN"));
|
||||||
|
g_assert_true(identity_traceability_identification_status_valid("disputed"));
|
||||||
|
g_assert_false(identity_traceability_identification_status_valid("identified"));
|
||||||
|
g_assert_false(identity_traceability_relation_type_valid("is_author"));
|
||||||
|
g_assert_false(identity_traceability_field_is_projectable(
|
||||||
|
"accepted","uncertain","human_confirmed","SPECIMEN"));
|
||||||
|
g_assert_true(identity_traceability_field_is_projectable(
|
||||||
|
"modified","partial","human_confirmed","SPECIMEN"));
|
||||||
|
g_assert_null(document_authenticity_assessment_new(ASSESSMENT1,EVIDENCE,NULL,
|
||||||
|
"confirmed_forged",NULL,AT,NULL,NULL));
|
||||||
|
DocumentAuthenticityAssessment*a=document_authenticity_assessment_new(
|
||||||
|
ASSESSMENT1,EVIDENCE,NULL,"confirmed_forged","Justification SPECIMEN",AT,NULL,NULL);
|
||||||
|
g_assert_nonnull(a);DocumentAuthenticityAssessment*c=
|
||||||
|
document_authenticity_assessment_copy(a);g_assert_cmpstr(c->status,==,a->status);
|
||||||
|
document_authenticity_assessment_free(c);document_authenticity_assessment_free(a);
|
||||||
|
}
|
||||||
|
static void test_dao_history_and_relations(void)
|
||||||
|
{
|
||||||
|
Fixture f=fixture_new();GError*e=NULL;
|
||||||
|
IdentityTraceabilityDao*d=identity_traceability_dao_new(f.database);
|
||||||
|
DocumentAuthenticityAssessment*a1=document_authenticity_assessment_new(
|
||||||
|
ASSESSMENT1,EVIDENCE,RUN,"indeterminate",NULL,AT,NULL,"Note SPECIMEN");
|
||||||
|
DocumentAuthenticityAssessment*a2=document_authenticity_assessment_new(
|
||||||
|
ASSESSMENT2,EVIDENCE,RUN,"suspicious","Anomalie SPECIMEN",
|
||||||
|
"2026-07-30T11:00:00Z",ASSESSMENT1,NULL);
|
||||||
|
g_assert_true(identity_traceability_dao_insert_authenticity(d,a1,&e));
|
||||||
|
g_assert_no_error(e);g_assert_true(identity_traceability_dao_insert_authenticity(d,a2,&e));
|
||||||
|
GPtrArray*h=identity_traceability_dao_list_authenticity(d,EVIDENCE,&e);
|
||||||
|
g_assert_cmpuint(h->len,==,2);g_ptr_array_unref(h);
|
||||||
|
DocumentAuthenticityAssessment*current=
|
||||||
|
identity_traceability_dao_current_authenticity(d,EVIDENCE,&e);
|
||||||
|
g_assert_cmpstr(current->identifier,==,ASSESSMENT2);
|
||||||
|
PersonEvidenceFactualRelation*r=person_evidence_factual_relation_new(
|
||||||
|
FACT,PERSON,EVIDENCE,RUN,"identity_observed_in","Observation SPECIMEN",AT,TRUE);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
g_assert_cmpuint(roles->len,==,10);
|
||||||
|
for(guint i=0;i<roles->len;i++){
|
||||||
|
PersonRoleVocabularyEntry*role=g_ptr_array_index(roles,i);
|
||||||
|
g_assert_true(role->active);g_assert_nonnull(role->code);
|
||||||
|
g_assert_nonnull(role->label);g_assert_nonnull(role->description);
|
||||||
|
if(i>0)g_assert_cmpint(((PersonRoleVocabularyEntry*)
|
||||||
|
g_ptr_array_index(roles,i-1))->display_order,<,role->display_order);
|
||||||
|
}
|
||||||
|
g_ptr_array_unref(roles);
|
||||||
|
GPtrArray*statuses=identity_traceability_dao_list_identification_statuses(
|
||||||
|
d,FALSE,&e);g_assert_no_error(e);g_assert_cmpuint(statuses->len,==,6);
|
||||||
|
for(guint i=0;i<statuses->len;i++){
|
||||||
|
IdentificationStatusVocabularyEntry*status=g_ptr_array_index(statuses,i);
|
||||||
|
g_assert_true(status->active);g_assert_nonnull(status->description);
|
||||||
|
if(i>0)g_assert_cmpint(((IdentificationStatusVocabularyEntry*)
|
||||||
|
g_ptr_array_index(statuses,i-1))->display_order,<,status->display_order);
|
||||||
|
}
|
||||||
|
g_ptr_array_unref(statuses);
|
||||||
|
person_evidence_factual_relation_free(r);
|
||||||
|
document_authenticity_assessment_free(current);
|
||||||
|
document_authenticity_assessment_free(a2);document_authenticity_assessment_free(a1);
|
||||||
|
identity_traceability_dao_free(d);fixture_free(&f);
|
||||||
|
}
|
||||||
|
static void test_sqlite_negative_guards(void)
|
||||||
|
{
|
||||||
|
Fixture f=fixture_new();database_close(f.database);f.database=NULL;
|
||||||
|
sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
|
||||||
|
exec_ok(d,"PRAGMA foreign_keys=ON;");
|
||||||
|
char*before=scalar(d,"SELECT raw_value FROM identity_field_observations WHERE id='" FIELD "';");
|
||||||
|
char*message=NULL;
|
||||||
|
g_assert_cmpint(sqlite3_exec(d,"INSERT INTO document_authenticity_assessments"
|
||||||
|
"(id,evidence_id,status,assessed_at,origin) VALUES("
|
||||||
|
"'80000000-0000-4000-8000-000000000018','" EVIDENCE "',"
|
||||||
|
"'confirmed_forged','" AT "','human');",NULL,NULL,&message),!=,SQLITE_OK);
|
||||||
|
sqlite3_free(message);message=NULL;
|
||||||
|
g_assert_cmpint(sqlite3_exec(d,"INSERT INTO person_evidence_factual_relations"
|
||||||
|
"(id,person_id,evidence_id,relation_type,observed_at,origin) VALUES("
|
||||||
|
"'80000000-0000-4000-8000-000000000019','" PERSON "','" EVIDENCE "',"
|
||||||
|
"'is_author','" AT "','human');",NULL,NULL,&message),!=,SQLITE_OK);
|
||||||
|
sqlite3_free(message);message=NULL;
|
||||||
|
g_assert_cmpint(sqlite3_exec(d,"UPDATE identity_field_observations SET "
|
||||||
|
"confirmed_value='SPECIMEN',confirmation_state='human_confirmed',"
|
||||||
|
"value_quality='uncertain' WHERE id='" FIELD "';",NULL,NULL,&message),!=,SQLITE_OK);
|
||||||
|
sqlite3_free(message);
|
||||||
|
char*raw=scalar(d,"SELECT raw_value FROM identity_field_observations WHERE id='" FIELD "';");
|
||||||
|
char*confirmed=scalar(d,"SELECT COALESCE(confirmed_value,'NULL') FROM "
|
||||||
|
"identity_field_observations WHERE id='" FIELD "';");
|
||||||
|
char*auth=scalar(d,"SELECT COUNT(*) FROM document_authenticity_assessments;");
|
||||||
|
char*relations=scalar(d,"SELECT COUNT(*) FROM person_evidence_factual_relations;");
|
||||||
|
g_assert_cmpstr(raw,==,before);g_assert_cmpstr(confirmed,==,"NULL");
|
||||||
|
g_assert_cmpstr(auth,==,"0");g_assert_cmpstr(relations,==,"0");
|
||||||
|
exec_ok(d,"UPDATE identity_field_observations SET "
|
||||||
|
"confirmed_value='CONFIRMÉ SPECIMEN',confirmation_state='human_confirmed',"
|
||||||
|
"value_quality='partial' WHERE id='" FIELD "';");
|
||||||
|
g_free(before);g_free(raw);g_free(confirmed);g_free(auth);g_free(relations);
|
||||||
|
sqlite3_close(d);f.database=database_open(f.path);g_assert_nonnull(f.database);
|
||||||
|
g_assert_true(database_migrate_to_latest(f.database));
|
||||||
|
IdentityOcrDao*ocr=identity_ocr_dao_new(f.database);GError*error=NULL;
|
||||||
|
GPtrArray*confirmed_fields=identity_ocr_dao_list_confirmed_fields(
|
||||||
|
ocr,DOCUMENT,&error);g_assert_no_error(error);
|
||||||
|
g_assert_cmpuint(confirmed_fields->len,==,1);
|
||||||
|
IdentityFieldObservationRecord*record=g_ptr_array_index(confirmed_fields,0);
|
||||||
|
g_assert_cmpstr(record->confirmed_value,==,"CONFIRMÉ SPECIMEN");
|
||||||
|
g_ptr_array_unref(confirmed_fields);identity_ocr_dao_free(ocr);fixture_free(&f);
|
||||||
|
}
|
||||||
|
static void test_migrate_v17_preserves_ocr(void)
|
||||||
|
{
|
||||||
|
Fixture f=fixture_new();database_close(f.database);f.database=NULL;
|
||||||
|
sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
|
||||||
|
exec_ok(d,"PRAGMA foreign_keys=OFF;"
|
||||||
|
"DROP TABLE person_identification_assessments;"
|
||||||
|
"DROP TABLE person_evidence_factual_relations;"
|
||||||
|
"DROP TABLE document_authenticity_assessments;"
|
||||||
|
"DROP TABLE person_role_vocabulary;DROP TABLE identification_status_vocabulary;"
|
||||||
|
"ALTER TABLE identity_field_observations RENAME TO identity_fields_v18_old;"
|
||||||
|
"CREATE TABLE identity_field_observations(id TEXT PRIMARY KEY,"
|
||||||
|
"observation_id TEXT NOT NULL,field_code TEXT NOT NULL,raw_value TEXT,"
|
||||||
|
"corrected_value TEXT,normalized_value TEXT,confidence REAL,"
|
||||||
|
"review_status TEXT NOT NULL,origin TEXT NOT NULL,evidence_id TEXT NOT NULL,"
|
||||||
|
"ocr_run_id TEXT NOT NULL,page_number INTEGER NOT NULL,source_x INTEGER,"
|
||||||
|
"source_y INTEGER,source_width INTEGER,source_height INTEGER,"
|
||||||
|
"source_image_width INTEGER,source_image_height INTEGER,display_order INTEGER,"
|
||||||
|
"reviewed_at TEXT NOT NULL,review_note TEXT);"
|
||||||
|
"INSERT INTO identity_field_observations SELECT id,observation_id,field_code,"
|
||||||
|
"raw_value,'CORRIGÉ SPECIMEN',normalized_value,confidence,review_status,origin,"
|
||||||
|
"evidence_id,ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
|
||||||
|
"source_image_width,source_image_height,display_order,reviewed_at,review_note "
|
||||||
|
"FROM identity_fields_v18_old;DROP TABLE identity_fields_v18_old;"
|
||||||
|
"UPDATE metadata SET value='17' WHERE key='schema_version';"
|
||||||
|
"CREATE TABLE identification_status_vocabulary(dummy TEXT);");
|
||||||
|
sqlite3_close(d);f.database=database_open(f.path);g_assert_nonnull(f.database);
|
||||||
|
g_test_expect_message(NULL,G_LOG_LEVEL_WARNING,
|
||||||
|
"*Impossible d’installer la migration SQLite V18*");
|
||||||
|
g_assert_false(database_migrate_to_latest(f.database));
|
||||||
|
g_test_assert_expected_messages();database_close(f.database);
|
||||||
|
f.database=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
|
||||||
|
char*rolled_back=scalar(d,
|
||||||
|
"SELECT value FROM metadata WHERE key='schema_version';");
|
||||||
|
char*partial=scalar(d,"SELECT COUNT(*) FROM sqlite_master WHERE type='table' "
|
||||||
|
"AND name='document_authenticity_assessments';");
|
||||||
|
g_assert_cmpstr(rolled_back,==,"17");g_assert_cmpstr(partial,==,"0");
|
||||||
|
g_free(rolled_back);g_free(partial);
|
||||||
|
exec_ok(d,"DROP TABLE identification_status_vocabulary;");
|
||||||
|
sqlite3_close(d);f.database=database_open(f.path);g_assert_nonnull(f.database);
|
||||||
|
g_assert_true(database_migrate_to_latest(f.database));database_close(f.database);
|
||||||
|
f.database=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
|
||||||
|
char*version=scalar(d,"SELECT value FROM metadata WHERE key='schema_version';");
|
||||||
|
char*values=scalar(d,"SELECT raw_value||':'||normalized_value||':'||corrected_value"
|
||||||
|
"||':'||confirmation_state FROM identity_field_observations WHERE id='" FIELD "';");
|
||||||
|
g_assert_cmpstr(version,==,"18");
|
||||||
|
g_assert_cmpstr(values,==,"BRUT SPECIMEN:BRUT SPECIMEN:CORRIGÉ SPECIMEN:unconfirmed");
|
||||||
|
sqlite3_close(d);g_free(version);g_free(values);
|
||||||
|
f.database=database_open(f.path);g_assert_true(database_migrate_to_latest(f.database));
|
||||||
|
fixture_free(&f);
|
||||||
|
}
|
||||||
|
int main(int argc,char**argv)
|
||||||
|
{g_test_init(&argc,&argv,NULL);g_test_add_func("/v18/models",test_models);
|
||||||
|
g_test_add_func("/v18/dao-history-relations",test_dao_history_and_relations);
|
||||||
|
g_test_add_func("/v18/sqlite-negative-guards",test_sqlite_negative_guards);
|
||||||
|
g_test_add_func("/v18/migrate-v17",test_migrate_v17_preserves_ocr);
|
||||||
|
return g_test_run();}
|
||||||
|
|
@ -56,7 +56,8 @@ static void assert_empty_after_reopen(const char *database_path)
|
||||||
static const char *const tables[] = {
|
static const char *const tables[] = {
|
||||||
"entites", "preuves", "preuve_entites", "preuve_entite_sources",
|
"entites", "preuves", "preuve_entites", "preuve_entite_sources",
|
||||||
"person_role_assignments", "identity_ocr_runs",
|
"person_role_assignments", "identity_ocr_runs",
|
||||||
"identity_document_observations", "identity_field_observations"
|
"identity_document_observations", "identity_field_observations",
|
||||||
|
"person_evidence_factual_relations"
|
||||||
};
|
};
|
||||||
Database *database = database_open(database_path);
|
Database *database = database_open(database_path);
|
||||||
g_assert_nonnull(database);
|
g_assert_nonnull(database);
|
||||||
|
|
@ -100,6 +101,7 @@ static void test_failure_matrix(void)
|
||||||
{PERSON_CREATION_FAILURE_CREATE_SOURCE,0},
|
{PERSON_CREATION_FAILURE_CREATE_SOURCE,0},
|
||||||
{PERSON_CREATION_FAILURE_CREATE_SOURCE,1},
|
{PERSON_CREATION_FAILURE_CREATE_SOURCE,1},
|
||||||
{PERSON_CREATION_FAILURE_CREATE_SOURCE,2},
|
{PERSON_CREATION_FAILURE_CREATE_SOURCE,2},
|
||||||
|
{PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION,0},
|
||||||
{PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,0},
|
{PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,0},
|
||||||
{PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,0},
|
{PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,0},
|
||||||
{PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,0},
|
{PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,0},
|
||||||
|
|
@ -167,11 +169,23 @@ static void test_failure_matrix(void)
|
||||||
.confidence = 10,
|
.confidence = 10,
|
||||||
.role_assignments = roles
|
.role_assignments = roles
|
||||||
};
|
};
|
||||||
|
PersonCreationFactualRelationInput factual_relation = {
|
||||||
|
.evidence_selection_identifier =
|
||||||
|
person_evidence_selection_item_get_identifier(
|
||||||
|
person_evidence_selection_get(selection, 0)),
|
||||||
|
.ocr_run_identifier = identity_ocr_run_get_identifier(
|
||||||
|
g_ptr_array_index(runs, 0)),
|
||||||
|
.relation_type = "identity_observed_in",
|
||||||
|
.factual_note = "Choix humain SPECIMEN"
|
||||||
|
};
|
||||||
|
GPtrArray *factual_relations = g_ptr_array_new();
|
||||||
|
g_ptr_array_add(factual_relations, &factual_relation);
|
||||||
PersonCreationCoordinatorOptions options = {
|
PersonCreationCoordinatorOptions options = {
|
||||||
.failure_point = cases[scenario].point,
|
.failure_point = cases[scenario].point,
|
||||||
.failure_occurrence = cases[scenario].occurrence,
|
.failure_occurrence = cases[scenario].occurrence,
|
||||||
.inject_compensation_failure =
|
.inject_compensation_failure =
|
||||||
scenario == G_N_ELEMENTS(cases) - 1
|
scenario == G_N_ELEMENTS(cases) - 1,
|
||||||
|
.factual_relations = factual_relations
|
||||||
};
|
};
|
||||||
char *ocr_parent=g_build_filename(root,"02_Preuves_Traitees",
|
char *ocr_parent=g_build_filename(root,"02_Preuves_Traitees",
|
||||||
"OCR",NULL);
|
"OCR",NULL);
|
||||||
|
|
@ -223,6 +237,7 @@ static void test_failure_matrix(void)
|
||||||
g_free(contents);
|
g_free(contents);
|
||||||
}
|
}
|
||||||
g_ptr_array_unref(roles);
|
g_ptr_array_unref(roles);
|
||||||
|
g_ptr_array_unref(factual_relations);
|
||||||
g_ptr_array_unref(runs);
|
g_ptr_array_unref(runs);
|
||||||
person_evidence_selection_free(selection);
|
person_evidence_selection_free(selection);
|
||||||
g_ptr_array_unref(prepared);
|
g_ptr_array_unref(prepared);
|
||||||
|
|
@ -391,9 +406,25 @@ static void test_success_and_rollback(void)
|
||||||
g_ptr_array_add(coordinator_runs, coordinator_run);
|
g_ptr_array_add(coordinator_runs, coordinator_run);
|
||||||
PersonEvidenceSelection *task_selection =
|
PersonEvidenceSelection *task_selection =
|
||||||
person_evidence_selection_copy(selection);
|
person_evidence_selection_copy(selection);
|
||||||
|
PersonCreationFactualRelationInput explicit_relation = {
|
||||||
|
.evidence_selection_identifier =
|
||||||
|
person_evidence_selection_item_get_identifier(
|
||||||
|
person_evidence_selection_get(task_selection, 0)),
|
||||||
|
.ocr_run_identifier =
|
||||||
|
identity_ocr_run_get_identifier(coordinator_run),
|
||||||
|
.relation_type = "data_extracted_from",
|
||||||
|
.factual_note = "Choix humain explicite SPECIMEN"
|
||||||
|
};
|
||||||
|
GPtrArray *explicit_relations = g_ptr_array_new();
|
||||||
|
g_ptr_array_add(explicit_relations, &explicit_relation);
|
||||||
|
PersonCreationCoordinatorOptions explicit_options = {
|
||||||
|
.factual_relations = explicit_relations
|
||||||
|
};
|
||||||
database = database_open(database_path);
|
database = database_open(database_path);
|
||||||
result = person_creation_coordinator_execute(database, root, &person,
|
result = person_creation_coordinator_execute_with_options(
|
||||||
task_selection, coordinator_runs, NULL, &error);
|
database, root, &person, task_selection, coordinator_runs,
|
||||||
|
&explicit_options, NULL, &error);
|
||||||
|
g_ptr_array_unref(explicit_relations);
|
||||||
person_evidence_selection_free(task_selection);
|
person_evidence_selection_free(task_selection);
|
||||||
g_assert_no_error(error);
|
g_assert_no_error(error);
|
||||||
g_assert_nonnull(result);
|
g_assert_nonnull(result);
|
||||||
|
|
@ -405,6 +436,8 @@ static void test_success_and_rollback(void)
|
||||||
g_assert_cmpuint(count(database, "identity_ocr_runs"), ==, 1);
|
g_assert_cmpuint(count(database, "identity_ocr_runs"), ==, 1);
|
||||||
g_assert_cmpuint(count(database, "identity_document_observations"), ==, 1);
|
g_assert_cmpuint(count(database, "identity_document_observations"), ==, 1);
|
||||||
g_assert_cmpuint(count(database, "identity_field_observations"), ==, 2);
|
g_assert_cmpuint(count(database, "identity_field_observations"), ==, 2);
|
||||||
|
g_assert_cmpuint(count(database,
|
||||||
|
"person_evidence_factual_relations"), ==, 1);
|
||||||
char *ocr_root = g_build_filename(root, "02_Preuves_Traitees", "OCR",
|
char *ocr_root = g_build_filename(root, "02_Preuves_Traitees", "OCR",
|
||||||
identity_ocr_run_get_identifier(coordinator_run), NULL);
|
identity_ocr_run_get_identifier(coordinator_run), NULL);
|
||||||
char *ocr_text = g_build_filename(ocr_root, "ocr.txt", NULL);
|
char *ocr_text = g_build_filename(ocr_root, "ocr.txt", NULL);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue