feat(person): enrichir la création avec rôles et preuves
This commit is contained in:
parent
db9be54a1a
commit
dfe6afcfca
44 changed files with 2677 additions and 48 deletions
76
Makefile
76
Makefile
|
|
@ -14,7 +14,7 @@ CFLAGS = -std=c17 \
|
|||
-MP \
|
||||
$(shell $(PKG_CONFIG) --cflags gtk4 sqlite3)
|
||||
|
||||
LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3)
|
||||
LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3) -ljpeg
|
||||
|
||||
TEST_CFLAGS = -std=c17 \
|
||||
-Wall \
|
||||
|
|
@ -125,6 +125,13 @@ TEST_EVIDENCE_RECLASSIFICATION := tests/test_evidence_reclassification
|
|||
TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
|
||||
TEST_SOCIAL_PLATFORM := tests/test_social_platform
|
||||
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
|
||||
TEST_EVIDENCE_PREVIEW := tests/test_evidence_preview
|
||||
TEST_EVIDENCE_SELECTION_MODEL := tests/test_evidence_selection_model
|
||||
TEST_PERSON_CREATION_GUARD := tests/test_person_creation_guard
|
||||
TEST_PERSON_DIALOG_LIFECYCLE := tests/test_person_dialog_lifecycle
|
||||
TEST_CREATE_PERSON_DIALOG_GTK := tests/test_create_person_dialog_gtk
|
||||
TEST_PERSON_CONFIRMATION_SUMMARY := tests/test_person_confirmation_summary
|
||||
TEST_PERSON_ROLE_ASSIGNMENT_DAO := tests/test_person_role_assignment_dao
|
||||
TEST_EML_ANALYZER := tests/test_eml_analyzer
|
||||
TEST_EML_INTEGRATION := tests/test_eml_integration
|
||||
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
||||
|
|
@ -778,10 +785,12 @@ $(TEST_SOCIAL_PLATFORM): \
|
|||
$(TEST_PERSON_ENTITY_SERVICE): \
|
||||
tests/test_person_entity_service.c \
|
||||
src/core/person_entity_service.c \
|
||||
src/dao/person_role_assignment_dao.c \
|
||||
src/dao/entity_dao.c \
|
||||
src/dao/evidence_entity_dao.c \
|
||||
src/models/entity_record.c \
|
||||
src/models/evidence_observation.c \
|
||||
src/models/person_role_assignment.c \
|
||||
src/database/database.c \
|
||||
src/database/schema.c \
|
||||
src/database/statement.c \
|
||||
|
|
@ -789,6 +798,50 @@ $(TEST_PERSON_ENTITY_SERVICE): \
|
|||
src/database/error.c
|
||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
$(TEST_EVIDENCE_PREVIEW): tests/test_evidence_preview.c \
|
||||
src/core/evidence_preview.c \
|
||||
src/core/evidence_preview_task.c src/core/background_task.c \
|
||||
src/core/task_manager.c \
|
||||
src/core/evidence_integrity_verifier.c \
|
||||
src/core/file_hash.c src/models/evidence_record.c
|
||||
$(CC) $(CFLAGS) -Wpedantic $^ -o $@ $(LDFLAGS) -ljpeg
|
||||
|
||||
$(TEST_EVIDENCE_SELECTION_MODEL): tests/test_evidence_selection_model.c \
|
||||
src/models/evidence_selection_model.c src/models/evidence_record.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
$(TEST_PERSON_CREATION_GUARD): tests/test_person_creation_guard.c \
|
||||
src/core/person_creation_guard.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
$(TEST_PERSON_DIALOG_LIFECYCLE): tests/test_person_dialog_lifecycle.c \
|
||||
src/core/person_dialog_lifecycle.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
$(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
|
||||
src/views/create_person_dialog.c src/core/person_dialog_lifecycle.c \
|
||||
src/core/person_confirmation_summary.c \
|
||||
src/core/evidence_preview_task.c src/core/evidence_preview.c \
|
||||
src/core/evidence_integrity_verifier.c src/core/file_hash.c \
|
||||
src/core/background_task.c src/core/task_manager.c \
|
||||
src/models/evidence_selection_model.c src/models/evidence_record.c \
|
||||
src/models/person_role_assignment.c
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
|
||||
|
||||
$(TEST_PERSON_CONFIRMATION_SUMMARY): \
|
||||
tests/test_person_confirmation_summary.c \
|
||||
src/core/person_confirmation_summary.c src/models/evidence_record.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO): \
|
||||
tests/test_person_role_assignment_dao.c \
|
||||
src/dao/person_role_assignment_dao.c src/models/person_role_assignment.c \
|
||||
src/dao/entity_dao.c src/models/entity_record.c \
|
||||
src/dao/evidence_dao.c src/models/evidence_record.c \
|
||||
src/database/database.c src/database/schema.c src/database/statement.c \
|
||||
src/database/transaction.c src/database/error.c
|
||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
|
||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
|
|
@ -895,6 +948,13 @@ test: \
|
|||
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
|
||||
$(TEST_SOCIAL_PLATFORM) \
|
||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||
$(TEST_EVIDENCE_PREVIEW) \
|
||||
$(TEST_EVIDENCE_SELECTION_MODEL) \
|
||||
$(TEST_PERSON_CREATION_GUARD) \
|
||||
$(TEST_PERSON_DIALOG_LIFECYCLE) \
|
||||
$(TEST_CREATE_PERSON_DIALOG_GTK) \
|
||||
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
|
||||
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
|
||||
$(TEST_EML_ANALYZER) \
|
||||
$(TEST_EML_INTEGRATION) \
|
||||
$(TEST_IBAN_ANALYZER) \
|
||||
|
|
@ -969,6 +1029,13 @@ test: \
|
|||
@$(TEST_SOCIAL_ACCOUNT_SERVICE)
|
||||
@$(TEST_SOCIAL_PLATFORM)
|
||||
@$(TEST_PERSON_ENTITY_SERVICE)
|
||||
@$(TEST_EVIDENCE_PREVIEW)
|
||||
@$(TEST_EVIDENCE_SELECTION_MODEL)
|
||||
@$(TEST_PERSON_CREATION_GUARD)
|
||||
@$(TEST_PERSON_DIALOG_LIFECYCLE)
|
||||
@$(TEST_CREATE_PERSON_DIALOG_GTK)
|
||||
@$(TEST_PERSON_CONFIRMATION_SUMMARY)
|
||||
@$(TEST_PERSON_ROLE_ASSIGNMENT_DAO)
|
||||
@$(TEST_EML_ANALYZER)
|
||||
@$(TEST_EML_INTEGRATION)
|
||||
@$(TEST_IBAN_ANALYZER)
|
||||
|
|
@ -1050,6 +1117,13 @@ clean:
|
|||
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
|
||||
$(TEST_SOCIAL_PLATFORM) \
|
||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||
$(TEST_EVIDENCE_PREVIEW) \
|
||||
$(TEST_EVIDENCE_SELECTION_MODEL) \
|
||||
$(TEST_PERSON_CREATION_GUARD) \
|
||||
$(TEST_PERSON_DIALOG_LIFECYCLE) \
|
||||
$(TEST_CREATE_PERSON_DIALOG_GTK) \
|
||||
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
|
||||
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
|
||||
$(TEST_EML_ANALYZER) \
|
||||
$(TEST_EML_INTEGRATION) \
|
||||
$(TEST_EXTRACTION_DROP_SERVICE) \
|
||||
|
|
|
|||
|
|
@ -238,3 +238,30 @@ CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_entity
|
|||
ON preuve_entite_sources(entite_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_source
|
||||
ON preuve_entite_sources(source_kind, source_uuid);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS person_role_assignments
|
||||
(
|
||||
id TEXT PRIMARY KEY CHECK (length(trim(id)) > 0),
|
||||
entity_id TEXT NOT NULL,
|
||||
role_code TEXT NOT NULL CHECK (length(trim(role_code)) > 0),
|
||||
evidence_id TEXT,
|
||||
provenance_kind TEXT NOT NULL CHECK (
|
||||
provenance_kind IN ('manual', 'legacy_manual')
|
||||
),
|
||||
confidence INTEGER CHECK (
|
||||
confidence IS NULL OR confidence BETWEEN 0 AND 100
|
||||
),
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
|
||||
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20),
|
||||
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_person_role_assignments_semantic
|
||||
ON person_role_assignments(
|
||||
entity_id, role_code, COALESCE(evidence_id, ''), provenance_kind
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_person_role_assignments_entity
|
||||
ON person_role_assignments(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_person_role_assignments_evidence
|
||||
ON person_role_assignments(evidence_id);
|
||||
|
|
|
|||
46
database/schema_v14.sql
Normal file
46
database/schema_v14.sql
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/******************************************************************************
|
||||
* Schéma SQLite V14 — affectations contextuelles multiples des personnes.
|
||||
*
|
||||
* L'unicité porte sur (personne, rôle, preuve, provenance) : un même rôle
|
||||
* peut donc être justifié par plusieurs preuves distinctes.
|
||||
******************************************************************************/
|
||||
CREATE TABLE person_role_assignments
|
||||
(
|
||||
id TEXT PRIMARY KEY CHECK (length(trim(id)) > 0),
|
||||
entity_id TEXT NOT NULL,
|
||||
role_code TEXT NOT NULL CHECK (length(trim(role_code)) > 0),
|
||||
evidence_id TEXT,
|
||||
provenance_kind TEXT NOT NULL CHECK (
|
||||
provenance_kind IN ('manual', 'legacy_manual')
|
||||
),
|
||||
confidence INTEGER CHECK (
|
||||
confidence IS NULL OR confidence BETWEEN 0 AND 100
|
||||
),
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
|
||||
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20),
|
||||
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_person_role_assignments_semantic
|
||||
ON person_role_assignments(
|
||||
entity_id, role_code, COALESCE(evidence_id, ''), provenance_kind
|
||||
);
|
||||
CREATE INDEX idx_person_role_assignments_entity
|
||||
ON person_role_assignments(entity_id);
|
||||
CREATE INDEX idx_person_role_assignments_evidence
|
||||
ON person_role_assignments(evidence_id);
|
||||
|
||||
/* Les codes historiques sont conservés littéralement : aucune équivalence
|
||||
* approximative n'est appliquée pendant la migration. */
|
||||
INSERT INTO person_role_assignments(
|
||||
id, entity_id, role_code, evidence_id, provenance_kind,
|
||||
confidence, notes, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
|
||||
substr(lower(hex(randomblob(2))),2) || '-a' ||
|
||||
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
|
||||
entity_id, role, NULL, 'legacy_manual', NULL, NULL, updated_at, updated_at
|
||||
FROM person_roles;
|
||||
|
|
@ -2,7 +2,22 @@
|
|||
|
||||
> **Version :** 3.1
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **Schéma SQLite courant :** V13
|
||||
> **Schéma SQLite courant :** V14
|
||||
|
||||
## Personnes contextuelles — SQLite V14
|
||||
|
||||
`person_role_assignments` sépare les rôles contextuels des champs de l’entité.
|
||||
Une personne peut porter plusieurs rôles avec preuve facultative, provenance,
|
||||
confiance et notes. Les anciens codes sont copiés littéralement depuis
|
||||
`person_roles` avec la provenance `legacy_manual`. Le service crée l’entité,
|
||||
les rôles et le rattachement manuel dans une transaction unique.
|
||||
|
||||
Le cœur `EvidencePreview` vérifie l’intégrité avant décodage PNG/JPEG, applique
|
||||
des bornes et ne renvoie qu’un PNG mémoire réduit. Une `BackgroundTask`
|
||||
travaille hors du thread GTK ; la texture est créée sur le contexte principal.
|
||||
Le dialogue annule l’ancienne tâche et rejette les générations obsolètes.
|
||||
L’interface conserve une génération de session et les chemins stables du
|
||||
projet et de sa base. PDF, vidéo, EML avancé et OCR restent exclus.
|
||||
> **Statut :** architecture courante
|
||||
|
||||
---
|
||||
|
|
@ -301,7 +316,7 @@ validation du chemin
|
|||
↓
|
||||
création de l'arborescence
|
||||
↓
|
||||
initialisation transactionnelle de SQLite V13
|
||||
initialisation transactionnelle de SQLite V14
|
||||
↓
|
||||
création de l'identité de l'enquête
|
||||
↓
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
# Dépendances externes
|
||||
|
||||
`libjpeg` est utilisée pour le décodage borné des aperçus JPEG du ticket #109.
|
||||
PNG est décodé avec Cairo. Aucun outil documentaire externe n’est lancé par
|
||||
l’aperçu et aucun dérivé n’est persisté.
|
||||
|
||||
Ce fichier recense les outils utilisés par LabFy Investigation et leur
|
||||
installation. Il sera complété à chaque intégration d’un nouvel outil.
|
||||
|
||||
|
|
|
|||
|
|
@ -405,16 +405,16 @@ tests/test_database.c
|
|||
|
||||
### 9.2 Nouvelle version de schéma
|
||||
|
||||
Pour créer une nouvelle version après V13, par exemple V14 :
|
||||
Pour créer une nouvelle version après V14, par exemple V15 :
|
||||
|
||||
1. ajouter `database/schema_v13.sql` ;
|
||||
1. ajouter le nouveau fichier `database/schema_v15.sql` ;
|
||||
2. déclarer et implémenter `schema_install_v13()` ;
|
||||
3. ajouter `database_migrate_v12_to_v13()` ;
|
||||
4. raccorder la migration dans la boucle vers la version courante ;
|
||||
5. mettre à jour les constantes de version ;
|
||||
6. installer V13 lors de la création d'une base neuve ;
|
||||
6. installer V15 lors de la création d'une base neuve ;
|
||||
7. adapter `schema_current.sql` si nécessaire ;
|
||||
8. ajouter une fixture V13 vers V14 ;
|
||||
8. ajouter une fixture V14 vers V15 ;
|
||||
|
||||
La fixture V12 vers V13 vérifie le backfill `legacy_manual`. Les tests EML
|
||||
couvrent aussi le retrait isolé d'une source face à un rattachement manuel.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **État du projet :** développement actif
|
||||
> **Schéma SQLite courant :** V13
|
||||
> **Schéma SQLite courant :** V14
|
||||
|
||||
La tranche 1 du ticket #109 couvre les rôles contextuels multiples, la
|
||||
sélection d’une preuve existante, la création transactionnelle et la garde de
|
||||
session. Import intégré, PDF, vidéo, EML avancé, OCR et documents d’identité
|
||||
restent hors périmètre.
|
||||
> **Usage opérationnel :** non prêt pour la production
|
||||
|
||||
---
|
||||
|
|
@ -116,7 +121,7 @@ La branche `main` contient notamment :
|
|||
|
||||
- création, validation et ouverture d'enquêtes ;
|
||||
- session d'enquête remplaçable proprement ;
|
||||
- infrastructure SQLite et migrations jusqu'à V13 ;
|
||||
- infrastructure SQLite et migrations jusqu'à V14 ;
|
||||
- couche Database, DAO et services métier ;
|
||||
- import de preuves avec copie contrôlée et SHA-256 ;
|
||||
- vérification d'intégrité et reclassement des preuves ;
|
||||
|
|
|
|||
29
docs/TEST_MANUEL_TICKET_109_TRANCHE_1.md
Normal file
29
docs/TEST_MANUEL_TICKET_109_TRANCHE_1.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Test manuel — ticket #109, tranche 1
|
||||
|
||||
Utiliser uniquement une enquête de démonstration et des fichiers synthétiques
|
||||
marqués `SPECIMEN`.
|
||||
|
||||
1. Cliquer sur Annuler à chacune des quatre étapes doit fermer uniquement
|
||||
l’assistant et laisser la fenêtre principale utilisable. Refaire le contrôle
|
||||
avec la croix, puis pendant le chargement d’un aperçu.
|
||||
2. Ouvrir une enquête synthétique, puis demander l’ajout d’une personne.
|
||||
3. Vérifier les quatre étapes `Personne`, `Rôles`, `Preuve`, `Confirmation`.
|
||||
4. Revenir en arrière : les champs, rôles multiples et la preuve choisie
|
||||
doivent être conservés. Aucun rôle sensible ne doit être précoché.
|
||||
5. Rechercher par nom, description et type, combiner avec le filtre métier,
|
||||
puis revenir à `Tous les types`.
|
||||
6. Choisir des PNG et JPEG synthétiques : vérifier chargement, aperçu et
|
||||
métadonnées complètes, puis les états invalide et absent.
|
||||
7. Sélectionner rapidement plusieurs preuves : seul le dernier aperçu doit
|
||||
apparaître, puis fermer la fenêtre pendant un chargement.
|
||||
8. Choisir zéro puis une preuve existante. Aucun import, OCR, PDF, vidéo ou
|
||||
analyse EML avancée ne doit être proposé.
|
||||
9. Confirmer et vérifier que personne, rôles, rattachement et source `manual`
|
||||
apparaissent ensemble après actualisation.
|
||||
10. Changer d’enquête pendant un aperçu puis avant confirmation : le résultat
|
||||
tardif et la création doivent être refusés sans écriture ni rafraîchissement.
|
||||
|
||||
L’aperçu de tranche 1 accepte uniquement PNG/JPEG intègres. Il refuse les
|
||||
fichiers absents, modifiés, corrompus ou supérieurs aux limites de 25 Mio,
|
||||
12 000 pixels par côté et 40 millions de pixels. Le rendu mémoire est borné à
|
||||
1 024 × 1 024 et aucun dérivé n’est persisté.
|
||||
36
include/core/evidence_preview.h
Normal file
36
include/core/evidence_preview.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#ifndef LABFY_INVESTIGATION_EVIDENCE_PREVIEW_H
|
||||
#define LABFY_INVESTIGATION_EVIDENCE_PREVIEW_H
|
||||
#include <gio/gio.h>
|
||||
#include <glib.h>
|
||||
G_BEGIN_DECLS
|
||||
#define EVIDENCE_PREVIEW_MAX_FILE_BYTES (25U * 1024U * 1024U)
|
||||
#define EVIDENCE_PREVIEW_MAX_DIMENSION 12000
|
||||
#define EVIDENCE_PREVIEW_MAX_PIXELS 40000000U
|
||||
#define EVIDENCE_PREVIEW_MAX_EDGE 1024
|
||||
typedef struct {
|
||||
char *investigation_root_path;
|
||||
char *evidence_identifier;
|
||||
char *relative_path;
|
||||
char *expected_sha256;
|
||||
char *mime_type;
|
||||
guint64 request_generation;
|
||||
} EvidencePreviewRequest;
|
||||
typedef struct {
|
||||
char *evidence_identifier;
|
||||
guint64 request_generation;
|
||||
GBytes *png_bytes;
|
||||
gint width;
|
||||
gint height;
|
||||
} EvidencePreviewResult;
|
||||
EvidencePreviewRequest *evidence_preview_request_new(const char *root,
|
||||
const char *evidence_identifier, const char *relative_path,
|
||||
const char *sha256, const char *mime_type, guint64 generation);
|
||||
void evidence_preview_request_free(EvidencePreviewRequest *request);
|
||||
EvidencePreviewResult *evidence_preview_load(
|
||||
const EvidencePreviewRequest *request, GCancellable *cancellable,
|
||||
GError **error);
|
||||
void evidence_preview_result_free(EvidencePreviewResult *result);
|
||||
gboolean evidence_preview_result_matches(const EvidencePreviewResult *result,
|
||||
const char *evidence_identifier, guint64 generation);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
12
include/core/evidence_preview_task.h
Normal file
12
include/core/evidence_preview_task.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#ifndef LABFY_INVESTIGATION_EVIDENCE_PREVIEW_TASK_H
|
||||
#define LABFY_INVESTIGATION_EVIDENCE_PREVIEW_TASK_H
|
||||
#include "core/background_task.h"
|
||||
#include "core/evidence_preview.h"
|
||||
#include "core/task_manager.h"
|
||||
G_BEGIN_DECLS
|
||||
BackgroundTask *evidence_preview_task_start(TaskManager *manager,
|
||||
const EvidencePreviewRequest *request,
|
||||
BackgroundTaskCompletionCallback callback, gpointer callback_data,
|
||||
GDestroyNotify callback_data_destroy, GError **error);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
16
include/core/person_confirmation_summary.h
Normal file
16
include/core/person_confirmation_summary.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#ifndef LABFY_INVESTIGATION_PERSON_CONFIRMATION_SUMMARY_H
|
||||
#define LABFY_INVESTIGATION_PERSON_CONFIRMATION_SUMMARY_H
|
||||
|
||||
#include "models/evidence_record.h"
|
||||
#include <glib.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
char *person_confirmation_summary_build(const char *designation,
|
||||
const char *declared_name, const char *pseudonym,
|
||||
const char *identification_status, gint confidence, const char *notes,
|
||||
const GPtrArray *role_labels, const EvidenceRecord *evidence);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
13
include/core/person_creation_guard.h
Normal file
13
include/core/person_creation_guard.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef LABFY_INVESTIGATION_PERSON_CREATION_GUARD_H
|
||||
#define LABFY_INVESTIGATION_PERSON_CREATION_GUARD_H
|
||||
#include <glib.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef struct PersonCreationGuard PersonCreationGuard;
|
||||
PersonCreationGuard *person_creation_guard_new(guint64 generation,
|
||||
const char *root_path, const char *database_path);
|
||||
void person_creation_guard_free(PersonCreationGuard *guard);
|
||||
gboolean person_creation_guard_matches(const PersonCreationGuard *guard,
|
||||
gboolean session_is_open, guint64 generation, const char *root_path,
|
||||
const char *database_path);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
29
include/core/person_dialog_lifecycle.h
Normal file
29
include/core/person_dialog_lifecycle.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#ifndef LABFY_INVESTIGATION_PERSON_DIALOG_LIFECYCLE_H
|
||||
#define LABFY_INVESTIGATION_PERSON_DIALOG_LIFECYCLE_H
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct PersonDialogLifecycle PersonDialogLifecycle;
|
||||
|
||||
PersonDialogLifecycle *person_dialog_lifecycle_new(void);
|
||||
void person_dialog_lifecycle_free(PersonDialogLifecycle *lifecycle);
|
||||
guint64 person_dialog_lifecycle_begin_preview(
|
||||
PersonDialogLifecycle *lifecycle);
|
||||
gboolean person_dialog_lifecycle_cancel(PersonDialogLifecycle *lifecycle);
|
||||
gboolean person_dialog_lifecycle_complete(PersonDialogLifecycle *lifecycle);
|
||||
gboolean person_dialog_lifecycle_accepts_preview(
|
||||
const PersonDialogLifecycle *lifecycle, guint64 generation);
|
||||
gboolean person_dialog_lifecycle_is_cancelled(
|
||||
const PersonDialogLifecycle *lifecycle);
|
||||
gboolean person_dialog_lifecycle_is_finished(
|
||||
const PersonDialogLifecycle *lifecycle);
|
||||
gboolean person_dialog_lifecycle_has_preview(
|
||||
const PersonDialogLifecycle *lifecycle);
|
||||
guint64 person_dialog_lifecycle_get_generation(
|
||||
const PersonDialogLifecycle *lifecycle);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#define LABFY_INVESTIGATION_PERSON_ENTITY_SERVICE_H
|
||||
#include "database/database.h"
|
||||
#include "models/entity_record.h"
|
||||
#include "models/person_role_assignment.h"
|
||||
#include <glib.h>
|
||||
G_BEGIN_DECLS
|
||||
/** @brief Données factuelles utilisées pour créer une personne. */
|
||||
|
|
@ -18,6 +19,8 @@ typedef struct
|
|||
const char *notes;
|
||||
gint confidence;
|
||||
const char *evidence_identifier;
|
||||
/** PersonRoleAssignmentInput* empruntés, facultatif. */
|
||||
const GPtrArray *role_assignments;
|
||||
} PersonEntityInput;
|
||||
/**
|
||||
* @brief Crée une personne et son éventuelle liaison à une preuve.
|
||||
|
|
|
|||
20
include/dao/person_role_assignment_dao.h
Normal file
20
include/dao/person_role_assignment_dao.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#ifndef LABFY_INVESTIGATION_PERSON_ROLE_ASSIGNMENT_DAO_H
|
||||
#define LABFY_INVESTIGATION_PERSON_ROLE_ASSIGNMENT_DAO_H
|
||||
#include "database/database.h"
|
||||
#include "models/person_role_assignment.h"
|
||||
#include <glib.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef struct PersonRoleAssignmentDao PersonRoleAssignmentDao;
|
||||
PersonRoleAssignmentDao *person_role_assignment_dao_new(
|
||||
Database *database, GError **error);
|
||||
void person_role_assignment_dao_free(PersonRoleAssignmentDao *dao);
|
||||
gboolean person_role_assignment_dao_insert(PersonRoleAssignmentDao *dao,
|
||||
const char *entity_identifier, const PersonRoleAssignmentInput *input,
|
||||
GError **error);
|
||||
gboolean person_role_assignment_dao_insert_all(PersonRoleAssignmentDao *dao,
|
||||
const char *entity_identifier, const GPtrArray *inputs, GError **error);
|
||||
GPtrArray *person_role_assignment_dao_list_by_entity(
|
||||
PersonRoleAssignmentDao *dao, const char *entity_identifier,
|
||||
GError **error);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -94,6 +94,7 @@ bool schema_install_v10(Database *database);
|
|||
bool schema_install_v11(Database *database);
|
||||
bool schema_install_v12(Database *database);
|
||||
bool schema_install_v13(Database *database);
|
||||
bool schema_install_v14(Database *database);
|
||||
|
||||
/**
|
||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||
|
|
|
|||
|
|
@ -147,6 +147,13 @@ EvidenceRecord *evidence_record_new(
|
|||
GError **error
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Copie intégralement une preuve, métadonnées d’affichage incluses.
|
||||
*
|
||||
* @return Nouvelle preuve possédée par l’appelant, ou NULL.
|
||||
*/
|
||||
EvidenceRecord *evidence_record_copy(const EvidenceRecord *evidence_record);
|
||||
|
||||
/**
|
||||
* @brief Libère un modèle EvidenceRecord.
|
||||
*
|
||||
|
|
@ -203,6 +210,16 @@ const char *evidence_record_get_type_identifier(
|
|||
const EvidenceRecord *evidence_record
|
||||
);
|
||||
|
||||
/** @brief Enrichit les métadonnées d'affichage copiées par le modèle. */
|
||||
void evidence_record_set_display_metadata(EvidenceRecord *evidence_record,
|
||||
const char *type_label, const char *mime_type);
|
||||
/** @brief Retourne le libellé métier du type, ou son code à défaut. */
|
||||
const char *evidence_record_get_type_label(
|
||||
const EvidenceRecord *evidence_record);
|
||||
/** @brief Retourne le type MIME enregistré, éventuellement NULL. */
|
||||
const char *evidence_record_get_mime_type(
|
||||
const EvidenceRecord *evidence_record);
|
||||
|
||||
/**
|
||||
* @brief Retourne la taille observée de la preuve.
|
||||
*
|
||||
|
|
|
|||
23
include/models/evidence_selection_model.h
Normal file
23
include/models/evidence_selection_model.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#ifndef LABFY_INVESTIGATION_EVIDENCE_SELECTION_MODEL_H
|
||||
#define LABFY_INVESTIGATION_EVIDENCE_SELECTION_MODEL_H
|
||||
#include "models/evidence_record.h"
|
||||
#include <glib.h>
|
||||
G_BEGIN_DECLS
|
||||
typedef struct EvidenceSelectionModel EvidenceSelectionModel;
|
||||
/** Crée un modèle possédant des copies profondes des preuves fournies. */
|
||||
EvidenceSelectionModel *evidence_selection_model_new(const GPtrArray *records);
|
||||
void evidence_selection_model_free(EvidenceSelectionModel *model);
|
||||
void evidence_selection_model_set_query(EvidenceSelectionModel *model,
|
||||
const char *query);
|
||||
void evidence_selection_model_set_type(EvidenceSelectionModel *model,
|
||||
const char *type_code);
|
||||
GPtrArray *evidence_selection_model_list_visible(
|
||||
const EvidenceSelectionModel *model);
|
||||
gboolean evidence_selection_model_select(EvidenceSelectionModel *model,
|
||||
const char *identifier);
|
||||
const EvidenceRecord *evidence_selection_model_get_selected(
|
||||
const EvidenceSelectionModel *model);
|
||||
GPtrArray *evidence_selection_model_list_type_codes(
|
||||
const EvidenceSelectionModel *model);
|
||||
G_END_DECLS
|
||||
#endif
|
||||
35
include/models/person_role_assignment.h
Normal file
35
include/models/person_role_assignment.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef LABFY_INVESTIGATION_PERSON_ROLE_ASSIGNMENT_H
|
||||
#define LABFY_INVESTIGATION_PERSON_ROLE_ASSIGNMENT_H
|
||||
#include <glib.h>
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char *role_code;
|
||||
char *evidence_identifier;
|
||||
char *provenance_kind;
|
||||
gboolean has_confidence;
|
||||
gint confidence;
|
||||
char *notes;
|
||||
} PersonRoleAssignmentInput;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char *identifier;
|
||||
char *entity_identifier;
|
||||
PersonRoleAssignmentInput assignment;
|
||||
char *created_at;
|
||||
char *updated_at;
|
||||
} PersonRoleAssignmentRecord;
|
||||
|
||||
gboolean person_role_assignment_role_is_known(const char *role_code);
|
||||
const char *person_role_assignment_role_label(const char *role_code);
|
||||
gboolean person_role_assignment_input_is_valid(
|
||||
const PersonRoleAssignmentInput *input);
|
||||
PersonRoleAssignmentInput *person_role_assignment_input_copy(
|
||||
const PersonRoleAssignmentInput *input);
|
||||
void person_role_assignment_input_free(PersonRoleAssignmentInput *input);
|
||||
void person_role_assignment_record_free(PersonRoleAssignmentRecord *record);
|
||||
|
||||
G_END_DECLS
|
||||
#endif
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#define LABFY_INVESTIGATION_CREATE_PERSON_DIALOG_H
|
||||
#include "core/person_entity_service.h"
|
||||
#include "models/evidence_record.h"
|
||||
#include "core/task_manager.h"
|
||||
#include <gtk/gtk.h>
|
||||
G_BEGIN_DECLS
|
||||
/** @brief Résultat opaque possédé par le callback. */
|
||||
|
|
@ -13,6 +14,7 @@ typedef struct CreatePersonDialogResult CreatePersonDialogResult;
|
|||
/** @brief Callback appelé après validation ou annulation. */
|
||||
typedef void (*CreatePersonDialogCallback)(CreatePersonDialogResult *result,
|
||||
gpointer user_data);
|
||||
typedef gboolean (*CreatePersonDialogSessionCheck)(gpointer user_data);
|
||||
/**
|
||||
* @brief Présente le formulaire de création d'une personne.
|
||||
* @param parent Fenêtre parente.
|
||||
|
|
@ -22,8 +24,10 @@ typedef void (*CreatePersonDialogCallback)(CreatePersonDialogResult *result,
|
|||
* @return TRUE si le dialogue a été présenté.
|
||||
*/
|
||||
gboolean create_person_dialog_present(GtkWindow *parent,
|
||||
const GPtrArray *evidence_records, CreatePersonDialogCallback callback,
|
||||
gpointer user_data);
|
||||
const GPtrArray *evidence_records, const char *investigation_root_path,
|
||||
TaskManager *task_manager, CreatePersonDialogSessionCheck session_check,
|
||||
CreatePersonDialogCallback callback, gpointer user_data,
|
||||
GDestroyNotify user_data_destroy);
|
||||
/** @brief Libère un résultat. */
|
||||
void create_person_dialog_result_free(CreatePersonDialogResult *result);
|
||||
/** @brief Retourne les données empruntées du résultat. */
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
#include "core/relation_service.h"
|
||||
#include "core/social_account_service.h"
|
||||
#include "core/person_entity_service.h"
|
||||
#include "core/person_creation_guard.h"
|
||||
#include "core/eml_processing.h"
|
||||
#include "core/eml_integration.h"
|
||||
#include "core/eml_pipeline_task.h"
|
||||
|
|
@ -142,6 +143,7 @@ struct Application
|
|||
InvestigationGraphModel *graph_model;
|
||||
InvestigationGraphLayout *graph_layout;
|
||||
guint64 graph_load_generation;
|
||||
guint64 session_generation;
|
||||
gboolean graph_viewport_restored;
|
||||
guint graph_viewport_save_source_id;
|
||||
|
||||
|
|
@ -150,6 +152,20 @@ struct Application
|
|||
char *pending_entity_selection_identifier;
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Application *application;
|
||||
PersonCreationGuard *guard;
|
||||
} ApplicationPersonDialogContext;
|
||||
|
||||
static void application_person_dialog_context_free(
|
||||
ApplicationPersonDialogContext *context)
|
||||
{
|
||||
if (context == NULL) return;
|
||||
person_creation_guard_free(context->guard);
|
||||
g_free(context);
|
||||
}
|
||||
|
||||
static void application_on_evidence_selected(
|
||||
const char *evidence_identifier, gpointer user_data);
|
||||
|
||||
|
|
@ -2726,6 +2742,7 @@ static gboolean application_install_session(
|
|||
|
||||
application->tree_model = new_tree_model;
|
||||
application->session = new_session;
|
||||
application->session_generation++;
|
||||
application->graph_viewport_restored = FALSE;
|
||||
|
||||
main_window_set_tree_model(
|
||||
|
|
@ -5380,13 +5397,25 @@ static void application_on_add_social_account_requested(gpointer user_data)
|
|||
static void application_on_person_completed(
|
||||
CreatePersonDialogResult *result, gpointer user_data)
|
||||
{
|
||||
Application *application = user_data;
|
||||
const PersonEntityInput *input = create_person_dialog_result_get_input(result);
|
||||
ApplicationPersonDialogContext *context = user_data;
|
||||
Application *application = context != NULL ? context->application : NULL;
|
||||
const PersonEntityInput *input = NULL;
|
||||
const InvestigationProject *project = NULL;
|
||||
GError *error = NULL;
|
||||
char *identifier = NULL;
|
||||
if (result == NULL || application == NULL || application->session == NULL)
|
||||
{ create_person_dialog_result_free(result); return; }
|
||||
const char *active_root = NULL, *active_database = NULL;
|
||||
if (result == NULL) {
|
||||
return;
|
||||
}
|
||||
input = create_person_dialog_result_get_input(result);
|
||||
if (application == NULL || application->session == NULL)
|
||||
goto changed_session;
|
||||
project = investigation_session_get_project(application->session);
|
||||
active_root = investigation_project_get_root_path(project);
|
||||
active_database = investigation_project_get_database_path(project);
|
||||
if (!person_creation_guard_matches(context->guard, TRUE,
|
||||
application->session_generation, active_root, active_database))
|
||||
goto changed_session;
|
||||
if (!person_entity_service_create(
|
||||
investigation_session_get_database(application->session),
|
||||
input, &identifier, &error))
|
||||
|
|
@ -5402,6 +5431,24 @@ static void application_on_person_completed(
|
|||
}
|
||||
g_clear_error(&error); g_free(identifier);
|
||||
create_person_dialog_result_free(result);
|
||||
return;
|
||||
changed_session:
|
||||
application_present_error(application, "Personne non créée",
|
||||
"L'enquête active a changé depuis l'ouverture de l'assistant.");
|
||||
create_person_dialog_result_free(result);
|
||||
}
|
||||
|
||||
static gboolean application_person_dialog_session_matches(gpointer user_data)
|
||||
{
|
||||
ApplicationPersonDialogContext *context = user_data;
|
||||
Application *application = context != NULL ? context->application : NULL;
|
||||
const InvestigationProject *project;
|
||||
if (application == NULL || application->session == NULL) return FALSE;
|
||||
project = investigation_session_get_project(application->session);
|
||||
return person_creation_guard_matches(context->guard, TRUE,
|
||||
application->session_generation,
|
||||
investigation_project_get_root_path(project),
|
||||
investigation_project_get_database_path(project));
|
||||
}
|
||||
|
||||
/** @brief Charge les preuves puis ouvre le formulaire d'une personne. */
|
||||
|
|
@ -5410,6 +5457,8 @@ static void application_on_add_person_requested(gpointer user_data)
|
|||
Application *application = user_data;
|
||||
EvidenceDao *dao = NULL;
|
||||
GPtrArray *records = NULL;
|
||||
ApplicationPersonDialogContext *context = NULL;
|
||||
const InvestigationProject *project = NULL;
|
||||
GError *error = NULL;
|
||||
if (application == NULL || application->session == NULL) return;
|
||||
dao = evidence_dao_new(
|
||||
|
|
@ -5418,9 +5467,21 @@ static void application_on_add_person_requested(gpointer user_data)
|
|||
if (records == NULL)
|
||||
application_present_error(application, "Formulaire indisponible",
|
||||
error != NULL ? error->message : "Impossible de charger les preuves.");
|
||||
else
|
||||
else {
|
||||
project = investigation_session_get_project(application->session);
|
||||
context = g_new0(ApplicationPersonDialogContext, 1);
|
||||
context->application = application;
|
||||
context->guard = person_creation_guard_new(
|
||||
application->session_generation,
|
||||
investigation_project_get_root_path(project),
|
||||
investigation_project_get_database_path(project));
|
||||
create_person_dialog_present(main_window_get_window(application->main_window),
|
||||
records, application_on_person_completed, application);
|
||||
records, investigation_project_get_root_path(project),
|
||||
application->task_manager,
|
||||
application_person_dialog_session_matches,
|
||||
application_on_person_completed, context,
|
||||
(GDestroyNotify) application_person_dialog_context_free);
|
||||
}
|
||||
g_clear_pointer(&records, g_ptr_array_unref);
|
||||
evidence_dao_free(dao); g_clear_error(&error);
|
||||
}
|
||||
|
|
|
|||
213
src/core/evidence_preview.c
Normal file
213
src/core/evidence_preview.c
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
#include "core/evidence_preview.h"
|
||||
#include "core/evidence_integrity_verifier.h"
|
||||
#include <cairo.h>
|
||||
#include <stdio.h>
|
||||
#include <jpeglib.h>
|
||||
#include <setjmp.h>
|
||||
|
||||
static GQuark preview_error(void)
|
||||
{ return g_quark_from_static_string("evidence-preview-error"); }
|
||||
|
||||
typedef struct {
|
||||
struct jpeg_error_mgr base;
|
||||
jmp_buf jump;
|
||||
} PreviewJpegError;
|
||||
static void jpeg_failed(j_common_ptr info)
|
||||
{
|
||||
PreviewJpegError *error = (PreviewJpegError *) info->err;
|
||||
longjmp(error->jump, 1);
|
||||
}
|
||||
static cairo_status_t write_png(void *closure,
|
||||
const unsigned char *data, unsigned int length)
|
||||
{
|
||||
g_byte_array_append(closure, data, length);
|
||||
return CAIRO_STATUS_SUCCESS;
|
||||
}
|
||||
static gboolean surface_to_result(cairo_surface_t *source, gint source_width,
|
||||
gint source_height, const EvidencePreviewRequest *request,
|
||||
EvidencePreviewResult **out_result, GError **error)
|
||||
{
|
||||
double scale = MIN(1.0, MIN(
|
||||
(double) EVIDENCE_PREVIEW_MAX_EDGE / source_width,
|
||||
(double) EVIDENCE_PREVIEW_MAX_EDGE / source_height));
|
||||
gint width = MAX(1, (gint) (source_width * scale));
|
||||
gint height = MAX(1, (gint) (source_height * scale));
|
||||
cairo_surface_t *target = cairo_image_surface_create(
|
||||
CAIRO_FORMAT_ARGB32, width, height);
|
||||
cairo_t *cr = cairo_create(target);
|
||||
GByteArray *bytes = g_byte_array_new();
|
||||
cairo_scale(cr, scale, scale);
|
||||
cairo_set_source_surface(cr, source, 0, 0);
|
||||
cairo_paint(cr);
|
||||
cairo_destroy(cr);
|
||||
if (cairo_surface_write_to_png_stream(target, write_png, bytes) !=
|
||||
CAIRO_STATUS_SUCCESS) {
|
||||
g_set_error_literal(error, preview_error(), 4,
|
||||
"Impossible de produire l'aperçu en mémoire.");
|
||||
g_byte_array_unref(bytes); cairo_surface_destroy(target);
|
||||
return FALSE;
|
||||
}
|
||||
cairo_surface_destroy(target);
|
||||
*out_result = g_new0(EvidencePreviewResult, 1);
|
||||
(*out_result)->evidence_identifier =
|
||||
g_strdup(request->evidence_identifier);
|
||||
(*out_result)->request_generation = request->request_generation;
|
||||
(*out_result)->width = width; (*out_result)->height = height;
|
||||
(*out_result)->png_bytes = g_byte_array_free_to_bytes(bytes);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
EvidencePreviewRequest *evidence_preview_request_new(const char *root,
|
||||
const char *evidence_identifier, const char *relative_path,
|
||||
const char *sha256, const char *mime_type, guint64 generation)
|
||||
{
|
||||
if (root == NULL || !g_uuid_string_is_valid(evidence_identifier) ||
|
||||
relative_path == NULL || sha256 == NULL || mime_type == NULL)
|
||||
return NULL;
|
||||
EvidencePreviewRequest *r = g_new0(EvidencePreviewRequest, 1);
|
||||
r->investigation_root_path = g_strdup(root);
|
||||
r->evidence_identifier = g_strdup(evidence_identifier);
|
||||
r->relative_path = g_strdup(relative_path);
|
||||
r->expected_sha256 = g_strdup(sha256);
|
||||
r->mime_type = g_strdup(mime_type);
|
||||
r->request_generation = generation;
|
||||
return r;
|
||||
}
|
||||
void evidence_preview_request_free(EvidencePreviewRequest *r)
|
||||
{
|
||||
if (r == NULL) return;
|
||||
g_free(r->investigation_root_path); g_free(r->evidence_identifier);
|
||||
g_free(r->relative_path); g_free(r->expected_sha256);
|
||||
g_free(r->mime_type); g_free(r);
|
||||
}
|
||||
void evidence_preview_result_free(EvidencePreviewResult *r)
|
||||
{
|
||||
if (r == NULL) return;
|
||||
g_free(r->evidence_identifier);
|
||||
g_clear_pointer(&r->png_bytes, g_bytes_unref);
|
||||
g_free(r);
|
||||
}
|
||||
gboolean evidence_preview_result_matches(const EvidencePreviewResult *r,
|
||||
const char *identifier, guint64 generation)
|
||||
{
|
||||
return r != NULL && r->request_generation == generation &&
|
||||
g_strcmp0(r->evidence_identifier, identifier) == 0;
|
||||
}
|
||||
|
||||
EvidencePreviewResult *evidence_preview_load(
|
||||
const EvidencePreviewRequest *request, GCancellable *cancellable,
|
||||
GError **error)
|
||||
{
|
||||
EvidenceIntegrityVerificationResult *verification = NULL;
|
||||
EvidencePreviewResult *result = NULL;
|
||||
char *path = NULL;
|
||||
gint width = 0, height = 0;
|
||||
if (request == NULL ||
|
||||
!(g_str_equal(request->mime_type, "image/png") ||
|
||||
g_str_equal(request->mime_type, "image/jpeg"))) {
|
||||
g_set_error_literal(error, preview_error(), 1,
|
||||
"Aperçu indisponible pour ce format.");
|
||||
return NULL;
|
||||
}
|
||||
verification = evidence_integrity_verifier_verify(
|
||||
request->investigation_root_path, request->relative_path,
|
||||
request->expected_sha256, cancellable, error);
|
||||
if (verification == NULL) return NULL;
|
||||
if (evidence_integrity_verification_result_get_status(verification) !=
|
||||
EVIDENCE_INTEGRITY_STATUS_VALID) {
|
||||
g_set_error_literal(error, preview_error(), 2,
|
||||
"Aperçu refusé : l'intégrité de la preuve est invalide.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (evidence_integrity_verification_result_get_size_bytes(verification) >
|
||||
EVIDENCE_PREVIEW_MAX_FILE_BYTES) {
|
||||
g_set_error_literal(error, preview_error(), 3,
|
||||
"L'image dépasse la taille maximale autorisée.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (cancellable != NULL &&
|
||||
g_cancellable_set_error_if_cancelled(cancellable, error)) goto cleanup;
|
||||
path = g_build_filename(request->investigation_root_path,
|
||||
request->relative_path, NULL);
|
||||
if (g_str_equal(request->mime_type, "image/png")) {
|
||||
cairo_surface_t *surface = cairo_image_surface_create_from_png(path);
|
||||
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
|
||||
cairo_surface_destroy(surface);
|
||||
g_set_error_literal(error, preview_error(), 4,
|
||||
"Le fichier image est illisible ou corrompu.");
|
||||
goto cleanup;
|
||||
}
|
||||
width = cairo_image_surface_get_width(surface);
|
||||
height = cairo_image_surface_get_height(surface);
|
||||
if (width > EVIDENCE_PREVIEW_MAX_DIMENSION ||
|
||||
height > EVIDENCE_PREVIEW_MAX_DIMENSION ||
|
||||
(guint64) width * (guint64) height > EVIDENCE_PREVIEW_MAX_PIXELS) {
|
||||
g_set_error_literal(error, preview_error(), 5,
|
||||
"Les dimensions de l'image dépassent les limites sûres.");
|
||||
cairo_surface_destroy(surface);
|
||||
goto cleanup;
|
||||
}
|
||||
surface_to_result(surface, width, height, request, &result, error);
|
||||
cairo_surface_destroy(surface);
|
||||
goto cleanup;
|
||||
}
|
||||
FILE *jpeg_file = fopen(path, "rb");
|
||||
struct jpeg_decompress_struct jpeg = {0};
|
||||
PreviewJpegError jpeg_error;
|
||||
cairo_surface_t *surface = NULL;
|
||||
if (jpeg_file == NULL) {
|
||||
g_set_error_literal(error, preview_error(), 4,
|
||||
"Le fichier image est illisible ou corrompu.");
|
||||
goto cleanup;
|
||||
}
|
||||
jpeg.err = jpeg_std_error(&jpeg_error.base);
|
||||
jpeg_error.base.error_exit = jpeg_failed;
|
||||
if (setjmp(jpeg_error.jump) != 0) {
|
||||
jpeg_destroy_decompress(&jpeg); fclose(jpeg_file);
|
||||
if (surface != NULL) cairo_surface_destroy(surface);
|
||||
g_set_error_literal(error, preview_error(), 4,
|
||||
"Le fichier JPEG est illisible ou corrompu.");
|
||||
goto cleanup;
|
||||
}
|
||||
jpeg_create_decompress(&jpeg); jpeg_stdio_src(&jpeg, jpeg_file);
|
||||
jpeg_read_header(&jpeg, TRUE);
|
||||
width = (gint) jpeg.image_width; height = (gint) jpeg.image_height;
|
||||
if (width > EVIDENCE_PREVIEW_MAX_DIMENSION ||
|
||||
height > EVIDENCE_PREVIEW_MAX_DIMENSION ||
|
||||
(guint64) width * (guint64) height > EVIDENCE_PREVIEW_MAX_PIXELS) {
|
||||
g_set_error_literal(error, preview_error(), 5,
|
||||
"Les dimensions de l'image dépassent les limites sûres.");
|
||||
jpeg_destroy_decompress(&jpeg); fclose(jpeg_file);
|
||||
goto cleanup;
|
||||
}
|
||||
jpeg.out_color_space = JCS_RGB;
|
||||
jpeg_start_decompress(&jpeg);
|
||||
surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
|
||||
unsigned char *pixels = cairo_image_surface_get_data(surface);
|
||||
gint stride = cairo_image_surface_get_stride(surface);
|
||||
JSAMPARRAY row = (*jpeg.mem->alloc_sarray)(
|
||||
(j_common_ptr) &jpeg, JPOOL_IMAGE, (JDIMENSION) width * 3U, 1);
|
||||
while (jpeg.output_scanline < jpeg.output_height) {
|
||||
jpeg_read_scanlines(&jpeg, row, 1);
|
||||
guint y = jpeg.output_scanline - 1;
|
||||
guint32 *destination = (guint32 *) (pixels + y * stride);
|
||||
for (gint x = 0; x < width; x++)
|
||||
destination[x] = ((guint32) row[0][x * 3] << 16) |
|
||||
((guint32) row[0][x * 3 + 1] << 8) | row[0][x * 3 + 2];
|
||||
if (cancellable != NULL && g_cancellable_is_cancelled(cancellable))
|
||||
break;
|
||||
}
|
||||
cairo_surface_mark_dirty(surface);
|
||||
jpeg_finish_decompress(&jpeg); jpeg_destroy_decompress(&jpeg);
|
||||
fclose(jpeg_file);
|
||||
if (cancellable != NULL &&
|
||||
g_cancellable_set_error_if_cancelled(cancellable, error)) {
|
||||
cairo_surface_destroy(surface); goto cleanup;
|
||||
}
|
||||
surface_to_result(surface, width, height, request, &result, error);
|
||||
cairo_surface_destroy(surface);
|
||||
cleanup:
|
||||
g_free(path);
|
||||
evidence_integrity_verification_result_free(verification);
|
||||
return result;
|
||||
}
|
||||
37
src/core/evidence_preview_task.c
Normal file
37
src/core/evidence_preview_task.c
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#include "core/evidence_preview_task.h"
|
||||
|
||||
static EvidencePreviewRequest *copy_request(const EvidencePreviewRequest *r)
|
||||
{
|
||||
return evidence_preview_request_new(r->investigation_root_path,
|
||||
r->evidence_identifier, r->relative_path, r->expected_sha256,
|
||||
r->mime_type, r->request_generation);
|
||||
}
|
||||
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
|
||||
gpointer data, gpointer *result, GError **error)
|
||||
{
|
||||
(void) task;
|
||||
*result = evidence_preview_load(data, cancellable, error);
|
||||
return *result != NULL;
|
||||
}
|
||||
BackgroundTask *evidence_preview_task_start(TaskManager *manager,
|
||||
const EvidencePreviewRequest *request,
|
||||
BackgroundTaskCompletionCallback callback, gpointer callback_data,
|
||||
GDestroyNotify callback_data_destroy, GError **error)
|
||||
{
|
||||
BackgroundTask *task = NULL;
|
||||
EvidencePreviewRequest *copy = NULL;
|
||||
if (manager == NULL || request == NULL) return NULL;
|
||||
task = background_task_new("Aperçu de la preuve");
|
||||
copy = copy_request(request);
|
||||
if (task == NULL || copy == NULL ||
|
||||
!task_manager_add(manager, task, error) ||
|
||||
!background_task_start(task, worker, copy,
|
||||
(GDestroyNotify) evidence_preview_request_free,
|
||||
(GDestroyNotify) evidence_preview_result_free,
|
||||
callback, callback_data, callback_data_destroy, error)) {
|
||||
evidence_preview_request_free(copy);
|
||||
background_task_unref(task);
|
||||
return NULL;
|
||||
}
|
||||
return task;
|
||||
}
|
||||
78
src/core/person_confirmation_summary.c
Normal file
78
src/core/person_confirmation_summary.c
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#include "core/person_confirmation_summary.h"
|
||||
|
||||
static const char *optional(const char *value, const char *fallback)
|
||||
{
|
||||
return value != NULL && value[0] != '\0' ? value : fallback;
|
||||
}
|
||||
|
||||
static const char *integrity(EvidenceIntegrityStatus status)
|
||||
{
|
||||
switch (status) {
|
||||
case EVIDENCE_INTEGRITY_STATUS_VALID: return "Valide";
|
||||
case EVIDENCE_INTEGRITY_STATUS_MISSING: return "Fichier absent";
|
||||
case EVIDENCE_INTEGRITY_STATUS_MODIFIED: return "Invalide";
|
||||
case EVIDENCE_INTEGRITY_STATUS_ERROR: return "Erreur";
|
||||
default: return "Non vérifiée";
|
||||
}
|
||||
}
|
||||
|
||||
char *person_confirmation_summary_build(const char *designation,
|
||||
const char *declared_name, const char *pseudonym,
|
||||
const char *identification_status, gint confidence, const char *notes,
|
||||
const GPtrArray *role_labels, const EvidenceRecord *evidence)
|
||||
{
|
||||
GString *summary = g_string_new(NULL);
|
||||
char *size = NULL, *short_sha = NULL;
|
||||
g_string_append_printf(summary,
|
||||
"PERSONNE\nDésignation : %s\nNom déclaré : %s\nPseudonyme : %s\n"
|
||||
"Statut d’identification : %s\nConfiance : %d %%\n"
|
||||
"Notes factuelles : %s\n\nRÔLES\n",
|
||||
optional(designation, "Non renseignée"),
|
||||
optional(declared_name, "Non renseigné"),
|
||||
optional(pseudonym, "Non renseigné"),
|
||||
optional(identification_status, "Non renseigné"),
|
||||
confidence, optional(notes, "Aucune"));
|
||||
if (role_labels == NULL || role_labels->len == 0)
|
||||
g_string_append(summary, "Aucun rôle sélectionné\n");
|
||||
else
|
||||
for (guint i = 0; i < role_labels->len; i++)
|
||||
g_string_append_printf(summary, "• %s — preuve : %s\n",
|
||||
optional(g_ptr_array_index((GPtrArray *) role_labels, i),
|
||||
"Rôle non renseigné"),
|
||||
evidence != NULL
|
||||
? optional(evidence_record_get_original_name(evidence),
|
||||
"Non renseignée")
|
||||
: "Aucune");
|
||||
g_string_append(summary, "\nPREUVE PRINCIPALE\n");
|
||||
if (evidence == NULL)
|
||||
g_string_append(summary, "Aucune preuve principale associée.\n");
|
||||
else {
|
||||
size = g_format_size(evidence_record_get_size_bytes(evidence));
|
||||
short_sha = g_strndup(evidence_record_get_sha256(evidence), 12);
|
||||
g_string_append_printf(summary,
|
||||
"Nom original : %s\nType métier : %s\nType MIME : %s\n"
|
||||
"Taille : %s\nDate d’import : %s\nSHA-256 : %s…\n"
|
||||
"Intégrité : %s\nDescription : %s\n",
|
||||
optional(evidence_record_get_original_name(evidence),
|
||||
"Non renseigné"),
|
||||
optional(evidence_record_get_type_label(evidence),
|
||||
evidence_record_get_type_identifier(evidence)),
|
||||
optional(evidence_record_get_mime_type(evidence),
|
||||
"Non renseigné"),
|
||||
size, optional(evidence_record_get_imported_at(evidence),
|
||||
"Non renseignée"),
|
||||
optional(short_sha, "Non renseigné"),
|
||||
integrity(evidence_record_get_integrity_status(evidence)),
|
||||
optional(evidence_record_get_description(evidence), "Aucune"));
|
||||
}
|
||||
g_string_append(summary,
|
||||
"\nAVERTISSEMENTS\n"
|
||||
"Aucune écriture n’a encore été effectuée.\n"
|
||||
"Les informations seront créées seulement après un clic sur "
|
||||
"« Créer la personne ».\n"
|
||||
"Aucune identité réelle ni relation avec un auteur n’est inférée "
|
||||
"automatiquement.");
|
||||
g_free(size);
|
||||
g_free(short_sha);
|
||||
return g_string_free(summary, FALSE);
|
||||
}
|
||||
35
src/core/person_creation_guard.c
Normal file
35
src/core/person_creation_guard.c
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#include "core/person_creation_guard.h"
|
||||
struct PersonCreationGuard {
|
||||
guint64 generation;
|
||||
char *root_path;
|
||||
char *database_path;
|
||||
};
|
||||
PersonCreationGuard *person_creation_guard_new(guint64 generation,
|
||||
const char *root_path, const char *database_path)
|
||||
{
|
||||
if (root_path == NULL || database_path == NULL) return NULL;
|
||||
PersonCreationGuard *g = g_new0(PersonCreationGuard, 1);
|
||||
g->generation = generation;
|
||||
g->root_path = g_canonicalize_filename(root_path, NULL);
|
||||
g->database_path = g_canonicalize_filename(database_path, NULL);
|
||||
return g;
|
||||
}
|
||||
void person_creation_guard_free(PersonCreationGuard *g)
|
||||
{
|
||||
if (g == NULL) return;
|
||||
g_free(g->root_path); g_free(g->database_path); g_free(g);
|
||||
}
|
||||
gboolean person_creation_guard_matches(const PersonCreationGuard *g,
|
||||
gboolean open, guint64 generation, const char *root, const char *database)
|
||||
{
|
||||
char *canonical_root, *canonical_database;
|
||||
gboolean matches;
|
||||
if (g == NULL || !open || root == NULL || database == NULL ||
|
||||
generation != g->generation) return FALSE;
|
||||
canonical_root = g_canonicalize_filename(root, NULL);
|
||||
canonical_database = g_canonicalize_filename(database, NULL);
|
||||
matches = g_strcmp0(canonical_root, g->root_path) == 0 &&
|
||||
g_strcmp0(canonical_database, g->database_path) == 0;
|
||||
g_free(canonical_root); g_free(canonical_database);
|
||||
return matches;
|
||||
}
|
||||
78
src/core/person_dialog_lifecycle.c
Normal file
78
src/core/person_dialog_lifecycle.c
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#include "core/person_dialog_lifecycle.h"
|
||||
|
||||
struct PersonDialogLifecycle
|
||||
{
|
||||
guint64 generation;
|
||||
gboolean preview_active;
|
||||
gboolean finished;
|
||||
gboolean cancelled;
|
||||
};
|
||||
|
||||
PersonDialogLifecycle *person_dialog_lifecycle_new(void)
|
||||
{
|
||||
return g_new0(PersonDialogLifecycle, 1);
|
||||
}
|
||||
|
||||
void person_dialog_lifecycle_free(PersonDialogLifecycle *lifecycle)
|
||||
{
|
||||
g_free(lifecycle);
|
||||
}
|
||||
|
||||
guint64 person_dialog_lifecycle_begin_preview(
|
||||
PersonDialogLifecycle *lifecycle)
|
||||
{
|
||||
if (lifecycle == NULL || lifecycle->finished) return 0;
|
||||
lifecycle->generation++;
|
||||
lifecycle->preview_active = TRUE;
|
||||
return lifecycle->generation;
|
||||
}
|
||||
|
||||
gboolean person_dialog_lifecycle_cancel(PersonDialogLifecycle *lifecycle)
|
||||
{
|
||||
if (lifecycle == NULL || lifecycle->finished) return FALSE;
|
||||
lifecycle->finished = TRUE;
|
||||
lifecycle->cancelled = TRUE;
|
||||
lifecycle->preview_active = FALSE;
|
||||
lifecycle->generation++;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean person_dialog_lifecycle_complete(PersonDialogLifecycle *lifecycle)
|
||||
{
|
||||
if (lifecycle == NULL || lifecycle->finished) return FALSE;
|
||||
lifecycle->finished = TRUE;
|
||||
lifecycle->preview_active = FALSE;
|
||||
lifecycle->generation++;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean person_dialog_lifecycle_accepts_preview(
|
||||
const PersonDialogLifecycle *lifecycle, guint64 generation)
|
||||
{
|
||||
return lifecycle != NULL && !lifecycle->finished &&
|
||||
lifecycle->preview_active && generation == lifecycle->generation;
|
||||
}
|
||||
|
||||
gboolean person_dialog_lifecycle_is_cancelled(
|
||||
const PersonDialogLifecycle *lifecycle)
|
||||
{
|
||||
return lifecycle != NULL && lifecycle->cancelled;
|
||||
}
|
||||
|
||||
gboolean person_dialog_lifecycle_is_finished(
|
||||
const PersonDialogLifecycle *lifecycle)
|
||||
{
|
||||
return lifecycle != NULL && lifecycle->finished;
|
||||
}
|
||||
|
||||
gboolean person_dialog_lifecycle_has_preview(
|
||||
const PersonDialogLifecycle *lifecycle)
|
||||
{
|
||||
return lifecycle != NULL && lifecycle->preview_active;
|
||||
}
|
||||
|
||||
guint64 person_dialog_lifecycle_get_generation(
|
||||
const PersonDialogLifecycle *lifecycle)
|
||||
{
|
||||
return lifecycle != NULL ? lifecycle->generation : 0;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
* @brief Création transactionnelle d'une personne observée.
|
||||
******************************************************************************/
|
||||
#include "core/person_entity_service.h"
|
||||
#include "dao/person_role_assignment_dao.h"
|
||||
#include "dao/entity_dao.h"
|
||||
#include "dao/evidence_entity_dao.h"
|
||||
#include "database/transaction.h"
|
||||
|
|
@ -188,6 +189,7 @@ gboolean person_entity_service_create(Database *database,
|
|||
{
|
||||
EntityDao *entity_dao = NULL;
|
||||
EvidenceEntityDao *link_dao = NULL;
|
||||
PersonRoleAssignmentDao *role_dao = NULL;
|
||||
EntityRecord *record = NULL;
|
||||
GDateTime *now = NULL;
|
||||
char *timestamp = NULL;
|
||||
|
|
@ -208,6 +210,16 @@ gboolean person_entity_service_create(Database *database,
|
|||
"Les informations de la personne sont invalides.");
|
||||
return FALSE;
|
||||
}
|
||||
for (guint i = 0; input->role_assignments != NULL &&
|
||||
i < input->role_assignments->len; i++)
|
||||
if (!person_role_assignment_input_is_valid(g_ptr_array_index(
|
||||
(GPtrArray *) input->role_assignments, i)))
|
||||
{
|
||||
g_set_error_literal(error, g_quark_from_static_string(
|
||||
"person-entity-service-error"), 1,
|
||||
"Une affectation de rôle est invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
value = input->declared_name != NULL && input->declared_name[0] != '\0'
|
||||
? input->declared_name
|
||||
: input->pseudonym != NULL && input->pseudonym[0] != '\0'
|
||||
|
|
@ -232,6 +244,10 @@ gboolean person_entity_service_create(Database *database,
|
|||
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
|
||||
if (entity_dao == NULL || record == NULL ||
|
||||
!entity_dao_insert(entity_dao, record, error)) goto cleanup;
|
||||
role_dao = person_role_assignment_dao_new(database, error);
|
||||
if (role_dao == NULL ||
|
||||
!person_role_assignment_dao_insert_all(role_dao, identifier,
|
||||
input->role_assignments, error)) goto cleanup;
|
||||
if (input->evidence_identifier != NULL && input->evidence_identifier[0] != '\0')
|
||||
{
|
||||
link_dao = evidence_entity_dao_new(database, error);
|
||||
|
|
@ -243,6 +259,7 @@ gboolean person_entity_service_create(Database *database,
|
|||
success = TRUE;
|
||||
if (out_identifier != NULL) *out_identifier = g_strdup(identifier);
|
||||
cleanup:
|
||||
person_role_assignment_dao_free(role_dao);
|
||||
if (!success && active) database_transaction_rollback(database);
|
||||
evidence_entity_dao_free(link_dao);
|
||||
entity_record_free(record);
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@ static const char *const evidence_dao_find_by_identifier_sql =
|
|||
" preuves.collected_at,"
|
||||
" preuves.source,"
|
||||
" preuves.description,"
|
||||
" preuves.integrity_status "
|
||||
" preuves.integrity_status,"
|
||||
" types_preuve.label,"
|
||||
" preuves.mime_type "
|
||||
"FROM preuves "
|
||||
"LEFT JOIN types_preuve "
|
||||
"ON types_preuve.id = preuves.type_id "
|
||||
|
|
@ -140,7 +142,9 @@ static const char *const evidence_dao_list_all_sql =
|
|||
" preuves.collected_at,"
|
||||
" preuves.source,"
|
||||
" preuves.description,"
|
||||
" preuves.integrity_status "
|
||||
" preuves.integrity_status,"
|
||||
" types_preuve.label,"
|
||||
" preuves.mime_type "
|
||||
"FROM preuves "
|
||||
"LEFT JOIN types_preuve "
|
||||
"ON types_preuve.id = preuves.type_id "
|
||||
|
|
@ -456,6 +460,8 @@ static EvidenceRecord *evidence_dao_read_current_record(
|
|||
char *collected_at = NULL;
|
||||
char *source = NULL;
|
||||
char *description = NULL;
|
||||
char *type_label = NULL;
|
||||
char *mime_type = NULL;
|
||||
|
||||
int64_t size_bytes = -1;
|
||||
int64_t integrity_status_value = -1;
|
||||
|
|
@ -534,6 +540,9 @@ static EvidenceRecord *evidence_dao_read_current_record(
|
|||
statement,
|
||||
11,
|
||||
&integrity_status_value
|
||||
) ||
|
||||
!database_statement_column_text(statement, 12, &type_label) ||
|
||||
!database_statement_column_text(statement, 13, &mime_type
|
||||
))
|
||||
{
|
||||
evidence_dao_set_error_literal(
|
||||
|
|
@ -608,6 +617,9 @@ static EvidenceRecord *evidence_dao_read_current_record(
|
|||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
evidence_record_set_display_metadata(
|
||||
evidence_record, type_label, mime_type);
|
||||
|
||||
cleanup:
|
||||
|
||||
|
|
@ -618,6 +630,8 @@ cleanup:
|
|||
g_free(
|
||||
description
|
||||
);
|
||||
g_free(type_label);
|
||||
g_free(mime_type);
|
||||
|
||||
g_free(
|
||||
source
|
||||
|
|
|
|||
142
src/dao/person_role_assignment_dao.c
Normal file
142
src/dao/person_role_assignment_dao.c
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#include "dao/person_role_assignment_dao.h"
|
||||
#include "database/statement.h"
|
||||
|
||||
struct PersonRoleAssignmentDao { Database *database; };
|
||||
static GQuark role_dao_error(void)
|
||||
{ return g_quark_from_static_string("person-role-assignment-dao-error"); }
|
||||
|
||||
PersonRoleAssignmentDao *person_role_assignment_dao_new(
|
||||
Database *database, GError **error)
|
||||
{
|
||||
if (database == NULL) {
|
||||
g_set_error_literal(error, role_dao_error(), 1,
|
||||
"La connexion SQLite est invalide.");
|
||||
return NULL;
|
||||
}
|
||||
PersonRoleAssignmentDao *dao = g_new0(PersonRoleAssignmentDao, 1);
|
||||
dao->database = database;
|
||||
return dao;
|
||||
}
|
||||
void person_role_assignment_dao_free(PersonRoleAssignmentDao *dao)
|
||||
{ g_free(dao); }
|
||||
|
||||
gboolean person_role_assignment_dao_insert(PersonRoleAssignmentDao *dao,
|
||||
const char *entity_identifier, const PersonRoleAssignmentInput *input,
|
||||
GError **error)
|
||||
{
|
||||
static const char sql[] =
|
||||
"INSERT INTO person_role_assignments(id,entity_id,role_code,"
|
||||
"evidence_id,provenance_kind,confidence,notes,created_at,updated_at) "
|
||||
"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') "
|
||||
"AND (? IS NULL OR EXISTS(SELECT 1 FROM preuves WHERE id=?)) "
|
||||
"RETURNING id;";
|
||||
DatabaseStatement *statement = NULL;
|
||||
GDateTime *now = NULL;
|
||||
char *timestamp = NULL, *identifier = NULL;
|
||||
gboolean success = FALSE;
|
||||
if (dao == NULL || !g_uuid_string_is_valid(entity_identifier) ||
|
||||
!person_role_assignment_input_is_valid(input)) goto invalid;
|
||||
now = g_date_time_new_now_utc();
|
||||
timestamp = now != NULL ? g_date_time_format(now,
|
||||
"%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||
identifier = g_uuid_string_random();
|
||||
statement = database_statement_prepare(dao->database, sql);
|
||||
if (statement == NULL || timestamp == NULL || identifier == NULL ||
|
||||
!database_statement_bind_text(statement, 1, identifier) ||
|
||||
!database_statement_bind_text(statement, 2, entity_identifier) ||
|
||||
!database_statement_bind_text(statement, 3, input->role_code) ||
|
||||
!(input->evidence_identifier != NULL
|
||||
? database_statement_bind_text(statement, 4, input->evidence_identifier)
|
||||
: database_statement_bind_null(statement, 4)) ||
|
||||
!database_statement_bind_text(statement, 5, input->provenance_kind) ||
|
||||
!(input->has_confidence
|
||||
? database_statement_bind_int64(statement, 6, input->confidence)
|
||||
: database_statement_bind_null(statement, 6)) ||
|
||||
!(input->notes != NULL
|
||||
? database_statement_bind_text(statement, 7, input->notes)
|
||||
: database_statement_bind_null(statement, 7)) ||
|
||||
!database_statement_bind_text(statement, 8, timestamp) ||
|
||||
!database_statement_bind_text(statement, 9, timestamp) ||
|
||||
!database_statement_bind_text(statement, 10, entity_identifier) ||
|
||||
!(input->evidence_identifier != NULL
|
||||
? database_statement_bind_text(statement, 11, input->evidence_identifier)
|
||||
: database_statement_bind_null(statement, 11)) ||
|
||||
!(input->evidence_identifier != NULL
|
||||
? database_statement_bind_text(statement, 12, input->evidence_identifier)
|
||||
: database_statement_bind_null(statement, 12)) ||
|
||||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW)
|
||||
goto failed;
|
||||
success = TRUE;
|
||||
goto cleanup;
|
||||
invalid:
|
||||
g_set_error_literal(error, role_dao_error(), 1,
|
||||
"L'affectation de rôle est invalide.");
|
||||
goto cleanup;
|
||||
failed:
|
||||
g_set_error_literal(error, role_dao_error(), 2,
|
||||
"La personne, la preuve ou l'affectation de rôle est invalide.");
|
||||
cleanup:
|
||||
database_statement_finalize(statement);
|
||||
g_free(identifier); g_free(timestamp);
|
||||
g_clear_pointer(&now, g_date_time_unref);
|
||||
return success;
|
||||
}
|
||||
|
||||
gboolean person_role_assignment_dao_insert_all(PersonRoleAssignmentDao *dao,
|
||||
const char *entity_identifier, const GPtrArray *inputs, GError **error)
|
||||
{
|
||||
for (guint i = 0; inputs != NULL && i < inputs->len; i++)
|
||||
if (!person_role_assignment_dao_insert(dao, entity_identifier,
|
||||
g_ptr_array_index((GPtrArray *) inputs, i), error))
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GPtrArray *person_role_assignment_dao_list_by_entity(
|
||||
PersonRoleAssignmentDao *dao, const char *entity_identifier, GError **error)
|
||||
{
|
||||
static const char sql[] = "SELECT id,role_code,evidence_id,provenance_kind,"
|
||||
"confidence,notes,created_at,updated_at FROM person_role_assignments "
|
||||
"WHERE entity_id=? ORDER BY created_at,id;";
|
||||
DatabaseStatement *s = NULL;
|
||||
GPtrArray *result = NULL;
|
||||
if (dao == NULL || !g_uuid_string_is_valid(entity_identifier)) goto failed;
|
||||
s = database_statement_prepare(dao->database, sql);
|
||||
result = g_ptr_array_new_with_free_func(
|
||||
(GDestroyNotify) person_role_assignment_record_free);
|
||||
if (s == NULL || result == NULL ||
|
||||
!database_statement_bind_text(s, 1, entity_identifier)) goto failed;
|
||||
while (database_statement_step(s) == DATABASE_STATEMENT_STEP_ROW) {
|
||||
PersonRoleAssignmentRecord *r = g_new0(PersonRoleAssignmentRecord, 1);
|
||||
int64_t confidence = 0;
|
||||
bool confidence_is_null = true;
|
||||
if (!database_statement_column_text(s, 0, &r->identifier) ||
|
||||
!database_statement_column_text(s, 1, &r->assignment.role_code) ||
|
||||
!database_statement_column_text(
|
||||
s, 2, &r->assignment.evidence_identifier) ||
|
||||
!database_statement_column_text(
|
||||
s, 3, &r->assignment.provenance_kind) ||
|
||||
!database_statement_column_is_null(s, 4, &confidence_is_null) ||
|
||||
!database_statement_column_text(s, 5, &r->assignment.notes) ||
|
||||
!database_statement_column_text(s, 6, &r->created_at) ||
|
||||
!database_statement_column_text(s, 7, &r->updated_at)) {
|
||||
person_role_assignment_record_free(r);
|
||||
goto failed;
|
||||
}
|
||||
r->entity_identifier = g_strdup(entity_identifier);
|
||||
r->assignment.has_confidence = !confidence_is_null;
|
||||
if (r->assignment.has_confidence &&
|
||||
database_statement_column_int64(s, 4, &confidence))
|
||||
r->assignment.confidence = (gint) confidence;
|
||||
g_ptr_array_add(result, r);
|
||||
}
|
||||
database_statement_finalize(s);
|
||||
return result;
|
||||
failed:
|
||||
database_statement_finalize(s);
|
||||
g_clear_pointer(&result, g_ptr_array_unref);
|
||||
g_set_error_literal(error, role_dao_error(), 2,
|
||||
"Impossible de lire les affectations de rôle.");
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -16,12 +16,12 @@
|
|||
/**
|
||||
* @brief Version actuelle du schéma SQLite.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 13
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 14
|
||||
|
||||
/**
|
||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "13"
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "14"
|
||||
|
||||
/**
|
||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||
|
|
@ -857,6 +857,21 @@ rollback:
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool database_migrate_v13_to_v14(Database *database)
|
||||
{
|
||||
bool transaction_started = false;
|
||||
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||
transaction_started = true;
|
||||
if (!schema_install_v14(database) ||
|
||||
!database_update_schema_version(database, "14") ||
|
||||
!database_transaction_commit(database)) goto rollback;
|
||||
return true;
|
||||
rollback:
|
||||
if (transaction_started && !database_transaction_rollback(database))
|
||||
g_warning("Impossible d’annuler la migration SQLite V13 vers V14.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||
*/
|
||||
|
|
@ -1098,6 +1113,10 @@ bool database_migrate_to_latest(
|
|||
if (!database_migrate_v12_to_v13(database)) return false;
|
||||
schema_version = 13;
|
||||
break;
|
||||
case 13:
|
||||
if (!database_migrate_v13_to_v14(database)) return false;
|
||||
schema_version = 14;
|
||||
break;
|
||||
|
||||
default:
|
||||
database_set_error(
|
||||
|
|
|
|||
|
|
@ -371,6 +371,12 @@ bool schema_install_v13(Database *database)
|
|||
"la migration SQLite V13");
|
||||
}
|
||||
|
||||
bool schema_install_v14(Database *database)
|
||||
{
|
||||
return schema_execute_file(database, "database/schema_v14.sql",
|
||||
"la migration SQLite V14");
|
||||
}
|
||||
|
||||
bool schema_ensure_current(
|
||||
Database *database
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ struct EvidenceRecord
|
|||
char *internal_name;
|
||||
char *relative_path;
|
||||
char *type_identifier;
|
||||
char *type_label;
|
||||
char *mime_type;
|
||||
|
||||
guint64 size_bytes;
|
||||
|
||||
|
|
@ -30,6 +32,28 @@ struct EvidenceRecord
|
|||
EvidenceIntegrityStatus integrity_status;
|
||||
};
|
||||
|
||||
EvidenceRecord *evidence_record_copy(const EvidenceRecord *evidence_record)
|
||||
{
|
||||
EvidenceRecord *copy;
|
||||
if (evidence_record == NULL) return NULL;
|
||||
copy = g_new0(EvidenceRecord, 1);
|
||||
copy->identifier = g_strdup(evidence_record->identifier);
|
||||
copy->original_name = g_strdup(evidence_record->original_name);
|
||||
copy->internal_name = g_strdup(evidence_record->internal_name);
|
||||
copy->relative_path = g_strdup(evidence_record->relative_path);
|
||||
copy->type_identifier = g_strdup(evidence_record->type_identifier);
|
||||
copy->type_label = g_strdup(evidence_record->type_label);
|
||||
copy->mime_type = g_strdup(evidence_record->mime_type);
|
||||
copy->size_bytes = evidence_record->size_bytes;
|
||||
copy->sha256 = g_strdup(evidence_record->sha256);
|
||||
copy->imported_at = g_strdup(evidence_record->imported_at);
|
||||
copy->collected_at = g_strdup(evidence_record->collected_at);
|
||||
copy->source = g_strdup(evidence_record->source);
|
||||
copy->description = g_strdup(evidence_record->description);
|
||||
copy->integrity_status = evidence_record->integrity_status;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Produit une erreur du domaine EvidenceRecord.
|
||||
*/
|
||||
|
|
@ -800,6 +824,8 @@ void evidence_record_free(
|
|||
g_free(
|
||||
evidence_record->type_identifier
|
||||
);
|
||||
g_free(evidence_record->type_label);
|
||||
g_free(evidence_record->mime_type);
|
||||
|
||||
g_free(
|
||||
evidence_record->relative_path
|
||||
|
|
@ -882,6 +908,28 @@ const char *evidence_record_get_type_identifier(
|
|||
return evidence_record->type_identifier;
|
||||
}
|
||||
|
||||
void evidence_record_set_display_metadata(EvidenceRecord *record,
|
||||
const char *type_label, const char *mime_type)
|
||||
{
|
||||
if (record == NULL) return;
|
||||
g_free(record->type_label);
|
||||
g_free(record->mime_type);
|
||||
record->type_label = g_strdup(type_label);
|
||||
record->mime_type = g_strdup(mime_type);
|
||||
}
|
||||
|
||||
const char *evidence_record_get_type_label(const EvidenceRecord *record)
|
||||
{
|
||||
if (record == NULL) return NULL;
|
||||
return record->type_label != NULL
|
||||
? record->type_label : record->type_identifier;
|
||||
}
|
||||
|
||||
const char *evidence_record_get_mime_type(const EvidenceRecord *record)
|
||||
{
|
||||
return record != NULL ? record->mime_type : NULL;
|
||||
}
|
||||
|
||||
guint64 evidence_record_get_size_bytes(
|
||||
const EvidenceRecord *evidence_record
|
||||
)
|
||||
|
|
|
|||
138
src/models/evidence_selection_model.c
Normal file
138
src/models/evidence_selection_model.c
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#include "models/evidence_selection_model.h"
|
||||
|
||||
struct EvidenceSelectionModel {
|
||||
GPtrArray *records;
|
||||
char *query;
|
||||
char *type_code;
|
||||
char *selected_identifier;
|
||||
};
|
||||
static gint compare_strings(gconstpointer left, gconstpointer right)
|
||||
{
|
||||
return g_strcmp0(*(char * const *) left, *(char * const *) right);
|
||||
}
|
||||
|
||||
static char *fold(const char *text)
|
||||
{
|
||||
char *normalized, *result;
|
||||
if (text == NULL) return g_strdup("");
|
||||
normalized = g_utf8_normalize(text, -1, G_NORMALIZE_ALL_COMPOSE);
|
||||
if (normalized == NULL) return g_strdup("");
|
||||
result = g_utf8_casefold(normalized, -1);
|
||||
g_free(normalized);
|
||||
return result != NULL ? result : g_strdup("");
|
||||
}
|
||||
static gboolean matches(const EvidenceSelectionModel *m,
|
||||
const EvidenceRecord *r)
|
||||
{
|
||||
gboolean found = FALSE;
|
||||
char *needle, *fields[5];
|
||||
if (m->type_code != NULL && g_strcmp0(m->type_code,
|
||||
evidence_record_get_type_identifier(r)) != 0) return FALSE;
|
||||
needle = fold(m->query);
|
||||
if (needle[0] == '\0') found = TRUE;
|
||||
fields[0] = fold(evidence_record_get_original_name(r));
|
||||
fields[1] = fold(evidence_record_get_description(r));
|
||||
fields[2] = fold(evidence_record_get_type_identifier(r));
|
||||
fields[3] = fold(evidence_record_get_type_label(r));
|
||||
fields[4] = fold(evidence_record_get_mime_type(r));
|
||||
for (guint i = 0; !found && i < G_N_ELEMENTS(fields); i++)
|
||||
found = strstr(fields[i], needle) != NULL;
|
||||
for (guint i = 0; i < G_N_ELEMENTS(fields); i++) g_free(fields[i]);
|
||||
g_free(needle);
|
||||
return found;
|
||||
}
|
||||
EvidenceSelectionModel *evidence_selection_model_new(const GPtrArray *records)
|
||||
{
|
||||
EvidenceSelectionModel *m = g_new0(EvidenceSelectionModel, 1);
|
||||
m->records = g_ptr_array_new_with_free_func(
|
||||
(GDestroyNotify) evidence_record_free);
|
||||
for (guint i = 0; records != NULL && i < records->len; i++) {
|
||||
EvidenceRecord *copy = evidence_record_copy(
|
||||
g_ptr_array_index((GPtrArray *) records, i));
|
||||
if (copy != NULL) g_ptr_array_add(m->records, copy);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
void evidence_selection_model_free(EvidenceSelectionModel *m)
|
||||
{
|
||||
if (m == NULL) return;
|
||||
g_ptr_array_unref(m->records); g_free(m->query);
|
||||
g_free(m->type_code); g_free(m->selected_identifier); g_free(m);
|
||||
}
|
||||
static void invalidate_hidden(EvidenceSelectionModel *m)
|
||||
{
|
||||
if (m->selected_identifier == NULL) return;
|
||||
for (guint i = 0; i < m->records->len; i++) {
|
||||
EvidenceRecord *r = g_ptr_array_index(m->records, i);
|
||||
if (g_strcmp0(evidence_record_get_identifier(r),
|
||||
m->selected_identifier) == 0 && matches(m, r)) return;
|
||||
}
|
||||
g_clear_pointer(&m->selected_identifier, g_free);
|
||||
}
|
||||
void evidence_selection_model_set_query(EvidenceSelectionModel *m,
|
||||
const char *query)
|
||||
{
|
||||
if (m == NULL) return;
|
||||
g_free(m->query); m->query = g_strdup(query); invalidate_hidden(m);
|
||||
}
|
||||
void evidence_selection_model_set_type(EvidenceSelectionModel *m,
|
||||
const char *type_code)
|
||||
{
|
||||
if (m == NULL) return;
|
||||
g_free(m->type_code);
|
||||
m->type_code = type_code != NULL && type_code[0] != '\0'
|
||||
? g_strdup(type_code) : NULL;
|
||||
invalidate_hidden(m);
|
||||
}
|
||||
GPtrArray *evidence_selection_model_list_visible(const EvidenceSelectionModel *m)
|
||||
{
|
||||
GPtrArray *result = g_ptr_array_new();
|
||||
for (guint i = 0; m != NULL && i < m->records->len; i++) {
|
||||
EvidenceRecord *r = g_ptr_array_index(m->records, i);
|
||||
if (matches(m, r)) g_ptr_array_add(result, r);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
gboolean evidence_selection_model_select(EvidenceSelectionModel *m,
|
||||
const char *identifier)
|
||||
{
|
||||
if (m == NULL) return FALSE;
|
||||
g_clear_pointer(&m->selected_identifier, g_free);
|
||||
if (identifier == NULL) return TRUE;
|
||||
for (guint i = 0; i < m->records->len; i++) {
|
||||
EvidenceRecord *r = g_ptr_array_index(m->records, i);
|
||||
if (matches(m, r) && g_strcmp0(
|
||||
evidence_record_get_identifier(r), identifier) == 0) {
|
||||
m->selected_identifier = g_strdup(identifier);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
const EvidenceRecord *evidence_selection_model_get_selected(
|
||||
const EvidenceSelectionModel *m)
|
||||
{
|
||||
for (guint i = 0; m != NULL && m->selected_identifier != NULL &&
|
||||
i < m->records->len; i++) {
|
||||
EvidenceRecord *r = g_ptr_array_index(m->records, i);
|
||||
if (g_strcmp0(evidence_record_get_identifier(r),
|
||||
m->selected_identifier) == 0) return r;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
GPtrArray *evidence_selection_model_list_type_codes(
|
||||
const EvidenceSelectionModel *m)
|
||||
{
|
||||
GPtrArray *types = g_ptr_array_new_with_free_func(g_free);
|
||||
for (guint i = 0; m != NULL && i < m->records->len; i++) {
|
||||
const char *code = evidence_record_get_type_identifier(
|
||||
g_ptr_array_index(m->records, i));
|
||||
gboolean exists = FALSE;
|
||||
for (guint j = 0; code != NULL && j < types->len; j++)
|
||||
if (g_strcmp0(code, g_ptr_array_index(types, j)) == 0)
|
||||
exists = TRUE;
|
||||
if (code != NULL && !exists) g_ptr_array_add(types, g_strdup(code));
|
||||
}
|
||||
g_ptr_array_sort(types, compare_strings);
|
||||
return types;
|
||||
}
|
||||
89
src/models/person_role_assignment.c
Normal file
89
src/models/person_role_assignment.c
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#include "models/person_role_assignment.h"
|
||||
#include <string.h>
|
||||
|
||||
typedef struct { const char *code; const char *label; } PersonRoleDefinition;
|
||||
static const PersonRoleDefinition roles[] = {
|
||||
{"alleged_author", "Auteur présumé"},
|
||||
{"presented_identity", "Identité présentée"},
|
||||
{"potentially_impersonated_identity", "Identité potentiellement usurpée"},
|
||||
{"victim", "Victime"},
|
||||
{"witness", "Témoin"},
|
||||
{"declared_bank_holder", "Titulaire bancaire déclaré"},
|
||||
{"intermediary", "Intermédiaire"},
|
||||
{"mentioned_person", "Personne citée"},
|
||||
{"other", "Autre"},
|
||||
/* Codes V6 conservés pour la lecture et la migration historique. */
|
||||
{"uncategorized", "Non catégorisée"},
|
||||
{"alleged_scammer", "Escroc présumé (historique)"},
|
||||
{"suspect", "Suspect (historique)"},
|
||||
{"related_person", "Personne liée (historique)"},
|
||||
{"impersonated_identity", "Identité usurpée (historique)"}
|
||||
};
|
||||
|
||||
gboolean person_role_assignment_role_is_known(const char *role_code)
|
||||
{
|
||||
if (role_code == NULL) return FALSE;
|
||||
for (guint i = 0; i < G_N_ELEMENTS(roles); i++)
|
||||
if (g_str_equal(role_code, roles[i].code)) return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
const char *person_role_assignment_role_label(const char *role_code)
|
||||
{
|
||||
for (guint i = 0; role_code != NULL && i < G_N_ELEMENTS(roles); i++)
|
||||
if (g_str_equal(role_code, roles[i].code)) return roles[i].label;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
gboolean person_role_assignment_input_is_valid(
|
||||
const PersonRoleAssignmentInput *input)
|
||||
{
|
||||
return input != NULL &&
|
||||
person_role_assignment_role_is_known(input->role_code) &&
|
||||
(input->evidence_identifier == NULL ||
|
||||
g_uuid_string_is_valid(input->evidence_identifier)) &&
|
||||
input->provenance_kind != NULL &&
|
||||
(g_str_equal(input->provenance_kind, "manual") ||
|
||||
g_str_equal(input->provenance_kind, "legacy_manual")) &&
|
||||
(!input->has_confidence ||
|
||||
(input->confidence >= 0 && input->confidence <= 100));
|
||||
}
|
||||
|
||||
PersonRoleAssignmentInput *person_role_assignment_input_copy(
|
||||
const PersonRoleAssignmentInput *input)
|
||||
{
|
||||
PersonRoleAssignmentInput *copy;
|
||||
if (input == NULL) return NULL;
|
||||
copy = g_new0(PersonRoleAssignmentInput, 1);
|
||||
copy->role_code = g_strdup(input->role_code);
|
||||
copy->evidence_identifier = g_strdup(input->evidence_identifier);
|
||||
copy->provenance_kind = g_strdup(input->provenance_kind);
|
||||
copy->has_confidence = input->has_confidence;
|
||||
copy->confidence = input->confidence;
|
||||
copy->notes = g_strdup(input->notes);
|
||||
return copy;
|
||||
}
|
||||
|
||||
void person_role_assignment_input_free(PersonRoleAssignmentInput *input)
|
||||
{
|
||||
if (input == NULL) return;
|
||||
g_free(input->role_code);
|
||||
g_free(input->evidence_identifier);
|
||||
g_free(input->provenance_kind);
|
||||
g_free(input->notes);
|
||||
g_free(input);
|
||||
}
|
||||
|
||||
void person_role_assignment_record_free(PersonRoleAssignmentRecord *record)
|
||||
{
|
||||
if (record == NULL) return;
|
||||
g_free(record->identifier);
|
||||
g_free(record->entity_identifier);
|
||||
g_free(record->assignment.role_code);
|
||||
g_free(record->assignment.evidence_identifier);
|
||||
g_free(record->assignment.provenance_kind);
|
||||
g_free(record->assignment.notes);
|
||||
g_free(record->created_at);
|
||||
g_free(record->updated_at);
|
||||
g_free(record);
|
||||
}
|
||||
|
|
@ -3,6 +3,10 @@
|
|||
* @brief Formulaire GTK de création d'une personne observée.
|
||||
******************************************************************************/
|
||||
#include "views/create_person_dialog.h"
|
||||
#include "core/evidence_preview_task.h"
|
||||
#include "core/person_confirmation_summary.h"
|
||||
#include "core/person_dialog_lifecycle.h"
|
||||
#include "models/evidence_selection_model.h"
|
||||
|
||||
struct CreatePersonDialogResult
|
||||
{
|
||||
|
|
@ -13,6 +17,7 @@ struct CreatePersonDialogResult
|
|||
char *status;
|
||||
char *notes;
|
||||
char *evidence_identifier;
|
||||
GPtrArray *role_assignments;
|
||||
};
|
||||
typedef struct
|
||||
{
|
||||
|
|
@ -23,14 +28,44 @@ typedef struct
|
|||
GtkDropDown *status;
|
||||
GtkSpinButton *confidence;
|
||||
GtkDropDown *evidence;
|
||||
GtkDropDown *type_filter;
|
||||
GtkEntry *search;
|
||||
GtkTextView *notes;
|
||||
GtkLabel *error;
|
||||
GtkLabel *metadata;
|
||||
GtkLabel *preview_status;
|
||||
GtkPicture *preview_picture;
|
||||
GtkLabel *progress;
|
||||
GtkLabel *summary;
|
||||
GtkStack *stack;
|
||||
GtkButton *previous;
|
||||
GtkButton *next;
|
||||
GtkButton *create;
|
||||
GtkCheckButton *roles[9];
|
||||
guint step;
|
||||
GPtrArray *evidence_identifiers;
|
||||
GPtrArray *visible_records;
|
||||
GPtrArray *type_codes;
|
||||
EvidenceSelectionModel *selection_model;
|
||||
char *investigation_root_path;
|
||||
TaskManager *task_manager;
|
||||
BackgroundTask *preview_task;
|
||||
PersonDialogLifecycle *lifecycle;
|
||||
CreatePersonDialogCallback callback;
|
||||
CreatePersonDialogSessionCheck session_check;
|
||||
gpointer user_data;
|
||||
gboolean completed;
|
||||
GDestroyNotify user_data_destroy;
|
||||
} CreatePersonDialogState;
|
||||
|
||||
typedef struct {
|
||||
GWeakRef window;
|
||||
char *evidence_identifier;
|
||||
guint64 generation;
|
||||
} CreatePersonPreviewContext;
|
||||
static const char *const status_codes[] = {"unknown", "suspected", "confirmed"};
|
||||
static void create_person_dialog_clear_preview(CreatePersonDialogState *state);
|
||||
static void create_person_dialog_select_record(CreatePersonDialogState *state,
|
||||
const EvidenceRecord *record);
|
||||
|
||||
/** @brief Copie et nettoie un texte facultatif. */
|
||||
static char *create_person_dialog_copy(const char *text)
|
||||
|
|
@ -54,16 +89,230 @@ static void create_person_dialog_state_free(gpointer data)
|
|||
{
|
||||
CreatePersonDialogState *state = data;
|
||||
if (state == NULL) return;
|
||||
if (state->preview_task != NULL) {
|
||||
background_task_cancel(state->preview_task);
|
||||
background_task_unref(state->preview_task);
|
||||
}
|
||||
person_dialog_lifecycle_cancel(state->lifecycle);
|
||||
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
|
||||
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
|
||||
g_clear_pointer(&state->type_codes, g_ptr_array_unref);
|
||||
g_clear_pointer(&state->selection_model, evidence_selection_model_free);
|
||||
g_clear_pointer(&state->lifecycle, person_dialog_lifecycle_free);
|
||||
g_free(state->investigation_root_path);
|
||||
if (state->user_data_destroy != NULL)
|
||||
state->user_data_destroy(state->user_data);
|
||||
g_free(state);
|
||||
}
|
||||
|
||||
static void create_person_dialog_rebuild_evidence(CreatePersonDialogState *state)
|
||||
{
|
||||
GtkStringList *labels = gtk_string_list_new(NULL);
|
||||
const EvidenceRecord *selected =
|
||||
evidence_selection_model_get_selected(state->selection_model);
|
||||
const char *selected_id = selected != NULL
|
||||
? evidence_record_get_identifier(selected) : NULL;
|
||||
guint selected_index = 0;
|
||||
gtk_string_list_append(labels, "Aucune preuve associée");
|
||||
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
|
||||
state->visible_records =
|
||||
evidence_selection_model_list_visible(state->selection_model);
|
||||
for (guint i = 0; i < state->visible_records->len; i++) {
|
||||
EvidenceRecord *record = g_ptr_array_index(state->visible_records, i);
|
||||
gtk_string_list_append(labels,
|
||||
evidence_record_get_original_name(record));
|
||||
if (g_strcmp0(selected_id,
|
||||
evidence_record_get_identifier(record)) == 0)
|
||||
selected_index = i + 1;
|
||||
}
|
||||
gtk_drop_down_set_model(state->evidence, G_LIST_MODEL(labels));
|
||||
gtk_drop_down_set_selected(state->evidence, selected_index);
|
||||
g_object_unref(labels);
|
||||
if (selected_index == 0) {
|
||||
evidence_selection_model_select(state->selection_model, NULL);
|
||||
create_person_dialog_clear_preview(state);
|
||||
}
|
||||
}
|
||||
|
||||
static void create_person_dialog_on_search_changed(
|
||||
GtkEditable *editable, gpointer data)
|
||||
{
|
||||
CreatePersonDialogState *state = data;
|
||||
evidence_selection_model_set_query(state->selection_model,
|
||||
gtk_editable_get_text(editable));
|
||||
create_person_dialog_rebuild_evidence(state);
|
||||
}
|
||||
|
||||
static void create_person_dialog_on_type_changed(
|
||||
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
|
||||
{
|
||||
CreatePersonDialogState *state = data;
|
||||
guint selected = gtk_drop_down_get_selected(dropdown);
|
||||
(void) pspec;
|
||||
evidence_selection_model_set_type(state->selection_model,
|
||||
selected > 0 && selected - 1 < state->type_codes->len
|
||||
? g_ptr_array_index(state->type_codes, selected - 1) : NULL);
|
||||
create_person_dialog_rebuild_evidence(state);
|
||||
}
|
||||
|
||||
static void create_person_dialog_on_evidence_changed(
|
||||
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
|
||||
{
|
||||
CreatePersonDialogState *state = data;
|
||||
guint selected = gtk_drop_down_get_selected(dropdown);
|
||||
(void) pspec;
|
||||
create_person_dialog_select_record(state,
|
||||
selected > 0 && selected - 1 < state->visible_records->len
|
||||
? g_ptr_array_index(state->visible_records, selected - 1) : NULL);
|
||||
}
|
||||
/** @brief Termine le dialogue comme une annulation. */
|
||||
static void create_person_dialog_cancel(CreatePersonDialogState *state)
|
||||
{
|
||||
if (state == NULL || state->completed) return;
|
||||
state->completed = TRUE;
|
||||
if (state == NULL ||
|
||||
!person_dialog_lifecycle_cancel(state->lifecycle)) return;
|
||||
if (state->preview_task != NULL) background_task_cancel(state->preview_task);
|
||||
if (state->callback != NULL) state->callback(NULL, state->user_data);
|
||||
}
|
||||
|
||||
static void preview_context_free(gpointer data)
|
||||
{
|
||||
CreatePersonPreviewContext *context = data;
|
||||
if (context == NULL) return;
|
||||
g_weak_ref_clear(&context->window);
|
||||
g_free(context->evidence_identifier);
|
||||
g_free(context);
|
||||
}
|
||||
|
||||
static const char *integrity_label(EvidenceIntegrityStatus status)
|
||||
{
|
||||
switch (status) {
|
||||
case EVIDENCE_INTEGRITY_STATUS_VALID: return "Valide";
|
||||
case EVIDENCE_INTEGRITY_STATUS_MISSING: return "Fichier absent";
|
||||
case EVIDENCE_INTEGRITY_STATUS_MODIFIED: return "Invalide";
|
||||
case EVIDENCE_INTEGRITY_STATUS_ERROR: return "Erreur de vérification";
|
||||
default: return "Non vérifiée";
|
||||
}
|
||||
}
|
||||
|
||||
static void create_person_dialog_clear_preview(CreatePersonDialogState *state)
|
||||
{
|
||||
if (state->preview_task != NULL) {
|
||||
background_task_cancel(state->preview_task);
|
||||
background_task_unref(state->preview_task);
|
||||
state->preview_task = NULL;
|
||||
}
|
||||
person_dialog_lifecycle_begin_preview(state->lifecycle);
|
||||
gtk_picture_set_paintable(state->preview_picture, NULL);
|
||||
gtk_label_set_text(state->preview_status, "Aucune preuve sélectionnée.");
|
||||
gtk_label_set_text(state->metadata, "");
|
||||
}
|
||||
|
||||
static void create_person_dialog_preview_completed(
|
||||
BackgroundTask *task, gpointer data)
|
||||
{
|
||||
CreatePersonPreviewContext *context = data;
|
||||
GtkWindow *window = g_weak_ref_get(&context->window);
|
||||
CreatePersonDialogState *state = window != NULL
|
||||
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
|
||||
EvidencePreviewResult *result = background_task_get_result(task);
|
||||
if (state != NULL && state->session_check != NULL &&
|
||||
!state->session_check(state->user_data)) {
|
||||
person_dialog_lifecycle_cancel(state->lifecycle);
|
||||
gtk_label_set_text(state->preview_status,
|
||||
"Aperçu annulé : l'enquête active a changé.");
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(state->create), FALSE);
|
||||
} else if (state != NULL &&
|
||||
person_dialog_lifecycle_accepts_preview(
|
||||
state->lifecycle, context->generation) &&
|
||||
evidence_preview_result_matches(result, context->evidence_identifier,
|
||||
context->generation)) {
|
||||
GError *error = NULL;
|
||||
GdkTexture *texture = gdk_texture_new_from_bytes(
|
||||
result->png_bytes, &error);
|
||||
if (texture != NULL) {
|
||||
gtk_picture_set_paintable(state->preview_picture,
|
||||
GDK_PAINTABLE(texture));
|
||||
gtk_label_set_text(state->preview_status, "Aperçu intègre.");
|
||||
g_object_unref(texture);
|
||||
} else {
|
||||
gtk_label_set_text(state->preview_status,
|
||||
error != NULL ? error->message : "Aperçu illisible.");
|
||||
}
|
||||
g_clear_error(&error);
|
||||
} else if (state != NULL &&
|
||||
person_dialog_lifecycle_accepts_preview(
|
||||
state->lifecycle, context->generation)) {
|
||||
GError *error = background_task_dup_error(task);
|
||||
gtk_label_set_text(state->preview_status,
|
||||
error != NULL ? error->message : "Aperçu annulé.");
|
||||
g_clear_error(&error);
|
||||
}
|
||||
if (state != NULL && state->preview_task == task) {
|
||||
background_task_unref(state->preview_task);
|
||||
state->preview_task = NULL;
|
||||
}
|
||||
g_clear_object(&window);
|
||||
}
|
||||
|
||||
static void create_person_dialog_select_record(CreatePersonDialogState *state,
|
||||
const EvidenceRecord *record)
|
||||
{
|
||||
char *size = NULL, *sha = NULL, *text = NULL;
|
||||
const char *mime;
|
||||
CreatePersonPreviewContext *context;
|
||||
EvidencePreviewRequest *request;
|
||||
GError *error = NULL;
|
||||
create_person_dialog_clear_preview(state);
|
||||
if (record == NULL) return;
|
||||
evidence_selection_model_select(state->selection_model,
|
||||
evidence_record_get_identifier(record));
|
||||
size = g_format_size(evidence_record_get_size_bytes(record));
|
||||
sha = g_strndup(evidence_record_get_sha256(record), 12);
|
||||
mime = evidence_record_get_mime_type(record);
|
||||
text = g_strdup_printf(
|
||||
"Nom : %s\nType : %s (%s)\nMIME : %s\nTaille : %s\n"
|
||||
"Import : %s\nSHA-256 : %s…\nIntégrité : %s\nDescription : %s",
|
||||
evidence_record_get_original_name(record),
|
||||
evidence_record_get_type_label(record),
|
||||
evidence_record_get_type_identifier(record),
|
||||
mime != NULL ? mime : "Non renseigné", size,
|
||||
evidence_record_get_imported_at(record), sha,
|
||||
integrity_label(evidence_record_get_integrity_status(record)),
|
||||
evidence_record_get_description(record) != NULL
|
||||
? evidence_record_get_description(record) : "Aucune");
|
||||
gtk_label_set_text(state->metadata, text);
|
||||
if (mime == NULL || !(g_str_equal(mime, "image/png") ||
|
||||
g_str_equal(mime, "image/jpeg"))) {
|
||||
gtk_label_set_text(state->preview_status,
|
||||
"Aperçu indisponible pour ce format.");
|
||||
goto cleanup;
|
||||
}
|
||||
gtk_label_set_text(state->preview_status,
|
||||
"Vérification et chargement en cours…");
|
||||
request = evidence_preview_request_new(state->investigation_root_path,
|
||||
evidence_record_get_identifier(record),
|
||||
evidence_record_get_relative_path(record),
|
||||
evidence_record_get_sha256(record), mime,
|
||||
person_dialog_lifecycle_get_generation(state->lifecycle));
|
||||
context = g_new0(CreatePersonPreviewContext, 1);
|
||||
g_weak_ref_init(&context->window, G_OBJECT(state->window));
|
||||
context->evidence_identifier =
|
||||
g_strdup(evidence_record_get_identifier(record));
|
||||
context->generation =
|
||||
person_dialog_lifecycle_get_generation(state->lifecycle);
|
||||
state->preview_task = evidence_preview_task_start(state->task_manager,
|
||||
request, create_person_dialog_preview_completed, context,
|
||||
preview_context_free, &error);
|
||||
evidence_preview_request_free(request);
|
||||
if (state->preview_task == NULL) {
|
||||
preview_context_free(context);
|
||||
gtk_label_set_text(state->preview_status,
|
||||
error != NULL ? error->message : "Aperçu impossible.");
|
||||
}
|
||||
g_clear_error(&error);
|
||||
cleanup:
|
||||
g_free(size); g_free(sha); g_free(text);
|
||||
}
|
||||
/** @brief Traite la fermeture native. */
|
||||
static gboolean create_person_dialog_on_close(GtkWindow *window, gpointer data)
|
||||
{
|
||||
|
|
@ -82,8 +331,16 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
|||
CreatePersonDialogResult *result = NULL;
|
||||
char *notes = NULL;
|
||||
guint status = gtk_drop_down_get_selected(state->status);
|
||||
guint evidence = gtk_drop_down_get_selected(state->evidence);
|
||||
const EvidenceRecord *selected_record =
|
||||
evidence_selection_model_get_selected(state->selection_model);
|
||||
(void) button;
|
||||
if (state->session_check != NULL &&
|
||||
!state->session_check(state->user_data)) {
|
||||
gtk_label_set_text(state->error,
|
||||
"L'enquête active a changé : création refusée.");
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||
return;
|
||||
}
|
||||
if (status >= G_N_ELEMENTS(status_codes) ||
|
||||
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0')
|
||||
{
|
||||
|
|
@ -100,9 +357,9 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
|||
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)));
|
||||
result->status = g_strdup(status_codes[status]);
|
||||
result->notes = create_person_dialog_copy(notes);
|
||||
if (evidence > 0 && evidence - 1 < state->evidence_identifiers->len)
|
||||
result->evidence_identifier = g_strdup(g_ptr_array_index(
|
||||
state->evidence_identifiers, evidence - 1));
|
||||
if (selected_record != NULL)
|
||||
result->evidence_identifier = g_strdup(
|
||||
evidence_record_get_identifier(selected_record));
|
||||
result->input.designation = result->designation;
|
||||
result->input.declared_name = result->declared_name;
|
||||
result->input.pseudonym = result->pseudonym;
|
||||
|
|
@ -110,11 +367,103 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
|
|||
result->input.notes = result->notes;
|
||||
result->input.confidence = gtk_spin_button_get_value_as_int(state->confidence);
|
||||
result->input.evidence_identifier = result->evidence_identifier;
|
||||
state->completed = TRUE;
|
||||
result->role_assignments = g_ptr_array_new_with_free_func(
|
||||
(GDestroyNotify) person_role_assignment_input_free);
|
||||
static const char *const role_codes[] = {
|
||||
"alleged_author", "presented_identity",
|
||||
"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;
|
||||
if (!person_dialog_lifecycle_complete(state->lifecycle)) {
|
||||
create_person_dialog_result_free(result);
|
||||
g_free(notes);
|
||||
return;
|
||||
}
|
||||
if (state->callback != NULL) state->callback(result, state->user_data);
|
||||
else create_person_dialog_result_free(result);
|
||||
g_free(notes); gtk_window_close(state->window);
|
||||
}
|
||||
|
||||
static void create_person_dialog_update_navigation(CreatePersonDialogState *state)
|
||||
{
|
||||
static const char *const pages[] = {"person", "roles", "evidence", "summary"};
|
||||
static const char *const steps[] = {
|
||||
"1 Personne", "2 Rôles", "3 Preuve", "4 Confirmation"};
|
||||
GString *progress = g_string_new(NULL);
|
||||
gtk_stack_set_visible_child_name(state->stack, pages[state->step]);
|
||||
for (guint i = 0; i < G_N_ELEMENTS(steps); i++) {
|
||||
char *escaped = g_markup_escape_text(steps[i], -1);
|
||||
if (i > 0) g_string_append(progress, " → ");
|
||||
if (i == state->step)
|
||||
g_string_append_printf(progress, "<b>%s</b>", escaped);
|
||||
else
|
||||
g_string_append(progress, escaped);
|
||||
g_free(escaped);
|
||||
}
|
||||
gtk_label_set_markup(state->progress, progress->str);
|
||||
g_string_free(progress, TRUE);
|
||||
if (state->step == 3) {
|
||||
static const char *const status_labels[] = {
|
||||
"Inconnu", "Présumé", "Confirmé"};
|
||||
GPtrArray *role_labels = g_ptr_array_new();
|
||||
const EvidenceRecord *evidence =
|
||||
evidence_selection_model_get_selected(state->selection_model);
|
||||
char *notes = create_person_dialog_get_notes(state);
|
||||
char *text;
|
||||
for (guint i = 0; i < G_N_ELEMENTS(state->roles); i++)
|
||||
if (gtk_check_button_get_active(state->roles[i]))
|
||||
g_ptr_array_add(role_labels, (gpointer)
|
||||
gtk_check_button_get_label(state->roles[i]));
|
||||
text = person_confirmation_summary_build(
|
||||
gtk_editable_get_text(GTK_EDITABLE(state->designation)),
|
||||
gtk_editable_get_text(GTK_EDITABLE(state->name)),
|
||||
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)),
|
||||
gtk_drop_down_get_selected(state->status) <
|
||||
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,
|
||||
role_labels, evidence);
|
||||
gtk_label_set_text(state->summary, text);
|
||||
g_free(text);
|
||||
g_free(notes);
|
||||
g_ptr_array_unref(role_labels);
|
||||
}
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(state->previous), state->step > 0);
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->next), state->step < 3);
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->create), state->step == 3);
|
||||
}
|
||||
static void create_person_dialog_on_previous(GtkButton *button, gpointer data)
|
||||
{
|
||||
CreatePersonDialogState *state = data; (void) button;
|
||||
if (state->step > 0) state->step--;
|
||||
create_person_dialog_update_navigation(state);
|
||||
}
|
||||
static void create_person_dialog_on_next(GtkButton *button, gpointer data)
|
||||
{
|
||||
CreatePersonDialogState *state = data; (void) button;
|
||||
if (state->step == 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_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||
return;
|
||||
}
|
||||
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
||||
if (state->step < 3) state->step++;
|
||||
create_person_dialog_update_navigation(state);
|
||||
}
|
||||
/** @brief Ajoute une ligne libellée au formulaire. */
|
||||
static void create_person_dialog_add_row(GtkGrid *grid, int row,
|
||||
const char *label, GtkWidget *widget)
|
||||
|
|
@ -126,17 +475,33 @@ static void create_person_dialog_add_row(GtkGrid *grid, int row,
|
|||
gtk_grid_attach(grid, widget, 1, row, 1, 1);
|
||||
}
|
||||
gboolean create_person_dialog_present(GtkWindow *parent,
|
||||
const GPtrArray *records, CreatePersonDialogCallback callback, gpointer data)
|
||||
const GPtrArray *records, const char *investigation_root_path,
|
||||
TaskManager *task_manager, CreatePersonDialogSessionCheck session_check,
|
||||
CreatePersonDialogCallback callback, gpointer data,
|
||||
GDestroyNotify data_destroy)
|
||||
{
|
||||
static const char *const statuses[] = {"Inconnu", "Présumé", "Confirmé", NULL};
|
||||
CreatePersonDialogState *state = NULL;
|
||||
GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL, *create = NULL;
|
||||
GtkStringList *labels = NULL;
|
||||
if (parent == NULL) return FALSE;
|
||||
GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL;
|
||||
GtkWidget *roles_box = NULL, *evidence_box = NULL, *summary = NULL;
|
||||
GtkStringList *labels = NULL, *type_labels = NULL;
|
||||
if (parent == NULL || investigation_root_path == NULL ||
|
||||
task_manager == NULL) return FALSE;
|
||||
state = g_new0(CreatePersonDialogState, 1);
|
||||
state->callback = callback; state->user_data = data;
|
||||
state->callback = callback; state->session_check = session_check;
|
||||
state->user_data = data; state->user_data_destroy = data_destroy;
|
||||
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
|
||||
state->selection_model = evidence_selection_model_new(records);
|
||||
state->visible_records =
|
||||
evidence_selection_model_list_visible(state->selection_model);
|
||||
state->type_codes =
|
||||
evidence_selection_model_list_type_codes(state->selection_model);
|
||||
state->investigation_root_path = g_strdup(investigation_root_path);
|
||||
state->task_manager = task_manager;
|
||||
state->lifecycle = person_dialog_lifecycle_new();
|
||||
state->window = GTK_WINDOW(gtk_window_new());
|
||||
gtk_window_set_application(state->window,
|
||||
gtk_window_get_application(parent));
|
||||
gtk_window_set_title(state->window, "Ajouter une personne");
|
||||
gtk_window_set_transient_for(state->window, parent);
|
||||
gtk_window_set_modal(state->window, TRUE);
|
||||
|
|
@ -154,10 +519,11 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
gtk_drop_down_set_selected(state->status, 1);
|
||||
state->confidence = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(0, 100, 5));
|
||||
gtk_spin_button_set_value(state->confidence, 30);
|
||||
labels = gtk_string_list_new(NULL); gtk_string_list_append(labels, "Aucune preuve associée");
|
||||
for (guint i = 0; records != NULL && i < records->len; i++)
|
||||
labels = gtk_string_list_new(NULL);
|
||||
gtk_string_list_append(labels, "Aucune preuve associée");
|
||||
for (guint i = 0; i < state->visible_records->len; i++)
|
||||
{
|
||||
EvidenceRecord *record = g_ptr_array_index((GPtrArray *) records, i);
|
||||
EvidenceRecord *record = g_ptr_array_index(state->visible_records, i);
|
||||
const char *id = evidence_record_get_identifier(record);
|
||||
const char *name = evidence_record_get_original_name(record);
|
||||
if (id == NULL || name == NULL) continue;
|
||||
|
|
@ -166,6 +532,22 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
}
|
||||
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(labels), NULL));
|
||||
labels = NULL;
|
||||
state->search = GTK_ENTRY(gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(state->search,
|
||||
"Rechercher par nom, description ou type");
|
||||
type_labels = gtk_string_list_new(NULL);
|
||||
gtk_string_list_append(type_labels, "Tous les types");
|
||||
for (guint i = 0; i < state->type_codes->len; i++)
|
||||
gtk_string_list_append(type_labels,
|
||||
g_ptr_array_index(state->type_codes, i));
|
||||
state->type_filter = GTK_DROP_DOWN(
|
||||
gtk_drop_down_new(G_LIST_MODEL(type_labels), NULL));
|
||||
/*
|
||||
* gtk_drop_down_new() prend la propriété complète du modèle. Une
|
||||
* libération ici laisserait les vues internes du GtkDropDown avec un
|
||||
* GListModel détruit et ferait échouer leur nettoyage à la fermeture.
|
||||
*/
|
||||
type_labels = NULL;
|
||||
state->notes = GTK_TEXT_VIEW(gtk_text_view_new());
|
||||
gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 90);
|
||||
|
|
@ -174,18 +556,80 @@ gboolean create_person_dialog_present(GtkWindow *parent,
|
|||
create_person_dialog_add_row(GTK_GRID(grid), 2, "Pseudonyme", GTK_WIDGET(state->pseudonym));
|
||||
create_person_dialog_add_row(GTK_GRID(grid), 3, "Identification", GTK_WIDGET(state->status));
|
||||
create_person_dialog_add_row(GTK_GRID(grid), 4, "Confiance (%)", GTK_WIDGET(state->confidence));
|
||||
create_person_dialog_add_row(GTK_GRID(grid), 5, "Preuve associée", GTK_WIDGET(state->evidence));
|
||||
create_person_dialog_add_row(GTK_GRID(grid), 6, "Notes factuelles", GTK_WIDGET(state->notes));
|
||||
create_person_dialog_add_row(GTK_GRID(grid), 5, "Notes factuelles", GTK_WIDGET(state->notes));
|
||||
state->stack = GTK_STACK(gtk_stack_new());
|
||||
gtk_stack_set_transition_type(state->stack,
|
||||
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
|
||||
gtk_stack_add_titled(state->stack, grid, "person", "1 — Personne");
|
||||
roles_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
static const char *const role_codes[] = {
|
||||
"alleged_author", "presented_identity",
|
||||
"potentially_impersonated_identity", "victim", "witness",
|
||||
"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");
|
||||
evidence_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_box_append(GTK_BOX(evidence_box), gtk_label_new(
|
||||
"Sélectionnez une preuve existante (aucun import dans cette version)."));
|
||||
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->search));
|
||||
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->type_filter));
|
||||
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->evidence));
|
||||
state->metadata = GTK_LABEL(gtk_label_new(""));
|
||||
gtk_label_set_xalign(state->metadata, 0.0f);
|
||||
gtk_label_set_selectable(state->metadata, TRUE);
|
||||
state->preview_status = GTK_LABEL(gtk_label_new(
|
||||
"Aucune preuve sélectionnée."));
|
||||
state->preview_picture = GTK_PICTURE(gtk_picture_new());
|
||||
gtk_picture_set_can_shrink(state->preview_picture, TRUE);
|
||||
gtk_picture_set_content_fit(state->preview_picture,
|
||||
GTK_CONTENT_FIT_CONTAIN);
|
||||
gtk_widget_set_size_request(GTK_WIDGET(state->preview_picture), 480, 260);
|
||||
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->metadata));
|
||||
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->preview_status));
|
||||
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->preview_picture));
|
||||
gtk_stack_add_titled(state->stack, evidence_box, "evidence", "3 — Preuve");
|
||||
state->summary = GTK_LABEL(gtk_label_new(""));
|
||||
summary = GTK_WIDGET(state->summary);
|
||||
gtk_label_set_wrap(state->summary, TRUE);
|
||||
gtk_label_set_xalign(state->summary, 0.0f);
|
||||
gtk_label_set_selectable(state->summary, TRUE);
|
||||
gtk_stack_add_titled(state->stack, summary, "summary", "4 — Confirmation");
|
||||
state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
||||
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||
cancel = gtk_button_new_with_label("Annuler"); create = gtk_button_new_with_label("Ajouter au graphe");
|
||||
gtk_widget_add_css_class(create, "suggested-action");
|
||||
gtk_box_append(GTK_BOX(actions), cancel); gtk_box_append(GTK_BOX(actions), create);
|
||||
gtk_box_append(GTK_BOX(box), grid); gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->error));
|
||||
cancel = gtk_button_new_with_label("Annuler");
|
||||
state->previous = GTK_BUTTON(gtk_button_new_with_label("Précédent"));
|
||||
state->next = GTK_BUTTON(gtk_button_new_with_label("Suivant"));
|
||||
state->create = GTK_BUTTON(gtk_button_new_with_label("Créer la personne"));
|
||||
gtk_widget_add_css_class(GTK_WIDGET(state->create), "suggested-action");
|
||||
gtk_box_append(GTK_BOX(actions), cancel);
|
||||
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->previous));
|
||||
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->next));
|
||||
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->create));
|
||||
state->progress = GTK_LABEL(gtk_label_new(NULL));
|
||||
gtk_label_set_xalign(state->progress, 0.0f);
|
||||
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->progress));
|
||||
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->stack));
|
||||
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->error));
|
||||
gtk_box_append(GTK_BOX(box), actions); gtk_window_set_child(state->window, box);
|
||||
g_signal_connect(state->window, "close-request", G_CALLBACK(create_person_dialog_on_close), state);
|
||||
g_signal_connect(cancel, "clicked", G_CALLBACK(create_person_dialog_on_cancel), state);
|
||||
g_signal_connect(create, "clicked", G_CALLBACK(create_person_dialog_on_create), state);
|
||||
g_signal_connect(state->previous, "clicked",
|
||||
G_CALLBACK(create_person_dialog_on_previous), state);
|
||||
g_signal_connect(state->next, "clicked",
|
||||
G_CALLBACK(create_person_dialog_on_next), state);
|
||||
g_signal_connect(state->create, "clicked",
|
||||
G_CALLBACK(create_person_dialog_on_create), state);
|
||||
g_signal_connect(state->search, "changed",
|
||||
G_CALLBACK(create_person_dialog_on_search_changed), state);
|
||||
g_signal_connect(state->type_filter, "notify::selected",
|
||||
G_CALLBACK(create_person_dialog_on_type_changed), state);
|
||||
g_signal_connect(state->evidence, "notify::selected",
|
||||
G_CALLBACK(create_person_dialog_on_evidence_changed), state);
|
||||
create_person_dialog_update_navigation(state);
|
||||
g_object_set_data_full(G_OBJECT(state->window), "person-dialog-state", state,
|
||||
create_person_dialog_state_free);
|
||||
gtk_window_present(state->window); return TRUE;
|
||||
|
|
@ -195,7 +639,9 @@ void create_person_dialog_result_free(CreatePersonDialogResult *result)
|
|||
if (result == NULL) return;
|
||||
g_free(result->designation); g_free(result->declared_name);
|
||||
g_free(result->pseudonym); g_free(result->status); g_free(result->notes);
|
||||
g_free(result->evidence_identifier); g_free(result);
|
||||
g_free(result->evidence_identifier);
|
||||
g_clear_pointer(&result->role_assignments, g_ptr_array_unref);
|
||||
g_free(result);
|
||||
}
|
||||
const PersonEntityInput *create_person_dialog_result_get_input(
|
||||
const CreatePersonDialogResult *result)
|
||||
|
|
|
|||
157
tests/test_create_person_dialog_gtk.c
Normal file
157
tests/test_create_person_dialog_gtk.c
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
#include "views/create_person_dialog.h"
|
||||
#include <gtk/gtk.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GtkApplication *application;
|
||||
GtkWindow *main_window;
|
||||
TaskManager *task_manager;
|
||||
guint completion_count;
|
||||
gboolean passed;
|
||||
} TestContext;
|
||||
|
||||
static GtkWidget *find_button(GtkWidget *widget, const char *label)
|
||||
{
|
||||
if (GTK_IS_BUTTON(widget) &&
|
||||
g_strcmp0(gtk_button_get_label(GTK_BUTTON(widget)), label) == 0)
|
||||
return widget;
|
||||
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||
GtkWidget *match = find_button(child, label);
|
||||
if (match != NULL) return match;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static GtkWidget *find_entry(GtkWidget *widget, const char *placeholder)
|
||||
{
|
||||
if (GTK_IS_ENTRY(widget) && g_strcmp0(
|
||||
gtk_entry_get_placeholder_text(GTK_ENTRY(widget)),
|
||||
placeholder) == 0)
|
||||
return widget;
|
||||
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||
GtkWidget *match = find_entry(child, placeholder);
|
||||
if (match != NULL) return match;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static GtkWidget *find_label_containing(GtkWidget *widget, const char *text)
|
||||
{
|
||||
if (GTK_IS_LABEL(widget) &&
|
||||
strstr(gtk_label_get_text(GTK_LABEL(widget)), text) != NULL)
|
||||
return widget;
|
||||
for (GtkWidget *child = gtk_widget_get_first_child(widget);
|
||||
child != NULL; child = gtk_widget_get_next_sibling(child)) {
|
||||
GtkWidget *match = find_label_containing(child, text);
|
||||
if (match != NULL) return match;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void completed(CreatePersonDialogResult *result, gpointer data)
|
||||
{
|
||||
TestContext *context = data;
|
||||
g_assert_null(result);
|
||||
context->completion_count++;
|
||||
}
|
||||
|
||||
static gboolean check_after_cancel(gpointer data)
|
||||
{
|
||||
TestContext *context = data;
|
||||
GList *windows = gtk_application_get_windows(context->application);
|
||||
g_assert_cmpuint(context->completion_count, ==, 1);
|
||||
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(context->main_window)));
|
||||
g_assert_cmpuint(g_list_length(windows), ==, 1);
|
||||
g_assert_true(windows->data == context->main_window);
|
||||
context->passed = TRUE;
|
||||
g_application_quit(G_APPLICATION(context->application));
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gboolean click_cancel(gpointer data)
|
||||
{
|
||||
TestContext *context = data;
|
||||
GList *windows = gtk_application_get_windows(context->application);
|
||||
GtkWindow *dialog = windows != NULL && windows->data != context->main_window
|
||||
? windows->data : windows != NULL ? windows->next->data : NULL;
|
||||
GtkWidget *button, *designation, *search;
|
||||
g_assert_nonnull(dialog);
|
||||
designation = find_entry(GTK_WIDGET(dialog),
|
||||
"Personne présumée liée aux comptes");
|
||||
g_assert_nonnull(designation);
|
||||
gtk_editable_set_text(GTK_EDITABLE(designation), "SPECIMEN");
|
||||
button = find_button(GTK_WIDGET(dialog), "Suivant");
|
||||
g_assert_nonnull(button);
|
||||
g_signal_emit_by_name(button, "clicked");
|
||||
g_signal_emit_by_name(button, "clicked");
|
||||
search = find_entry(GTK_WIDGET(dialog),
|
||||
"Rechercher par nom, description ou type");
|
||||
g_assert_nonnull(search);
|
||||
gtk_editable_set_text(GTK_EDITABLE(search), "SPECIMEN");
|
||||
button = find_button(GTK_WIDGET(dialog), "Suivant");
|
||||
g_signal_emit_by_name(button, "clicked");
|
||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||
"Désignation : SPECIMEN"));
|
||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||
"SPECIMEN-recherche.png"));
|
||||
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
|
||||
"Aucune écriture n’a encore été effectuée"));
|
||||
button = find_button(GTK_WIDGET(dialog), "Annuler");
|
||||
g_assert_nonnull(button);
|
||||
g_signal_emit_by_name(button, "clicked");
|
||||
g_idle_add(check_after_cancel, context);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void activate(GtkApplication *application, gpointer data)
|
||||
{
|
||||
TestContext *context = data;
|
||||
GError *error = NULL;
|
||||
GPtrArray *records = g_ptr_array_new_with_free_func(
|
||||
(GDestroyNotify) evidence_record_free);
|
||||
EvidenceRecord *record = evidence_record_new(
|
||||
"10000000-0000-4000-8000-000000000001",
|
||||
"SPECIMEN-recherche.png", "specimen.png",
|
||||
"01_Preuves_Originales/specimen.png", "screenshot", 12,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"2026-07-28T10:00:00Z", NULL, NULL,
|
||||
"Description synthétique", EVIDENCE_INTEGRITY_STATUS_UNKNOWN,
|
||||
&error);
|
||||
g_assert_no_error(error);
|
||||
evidence_record_set_display_metadata(
|
||||
record, "Capture d’écran", NULL);
|
||||
g_ptr_array_add(records, record);
|
||||
context->main_window = GTK_WINDOW(
|
||||
gtk_application_window_new(application));
|
||||
gtk_window_present(context->main_window);
|
||||
g_assert_true(create_person_dialog_present(context->main_window,
|
||||
records, "/tmp", context->task_manager, NULL, completed,
|
||||
context, NULL));
|
||||
g_ptr_array_unref(records);
|
||||
g_idle_add(click_cancel, context);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
TestContext context = {0};
|
||||
int status;
|
||||
if (!gtk_init_check()) {
|
||||
g_print("SKIP: aucun affichage GTK disponible.\n");
|
||||
return 0;
|
||||
}
|
||||
context.application = gtk_application_new(
|
||||
"org.labfy.Investigation.DialogTest",
|
||||
G_APPLICATION_NON_UNIQUE);
|
||||
context.task_manager = task_manager_new();
|
||||
g_signal_connect(context.application, "activate",
|
||||
G_CALLBACK(activate), &context);
|
||||
status = g_application_run(
|
||||
G_APPLICATION(context.application), argc, argv);
|
||||
g_assert_true(context.passed);
|
||||
task_manager_free(context.task_manager);
|
||||
g_object_unref(context.application);
|
||||
return status;
|
||||
}
|
||||
|
|
@ -525,7 +525,8 @@ static void test_database_initialize_valid_database(void)
|
|||
"FROM investigation;"
|
||||
);
|
||||
|
||||
assert(strcmp(schema_version, "13") == 0);
|
||||
assert(strcmp(schema_version, "14") == 0);
|
||||
test_database_assert_table_exists(database, "person_role_assignments");
|
||||
test_database_assert_table_exists(database, "bank_account_entities");
|
||||
test_database_assert_table_exists(database, "relation_types");
|
||||
test_database_assert_table_exists(database, "graph_viewport");
|
||||
|
|
@ -992,7 +993,7 @@ static void test_database_migrate_v1_to_v2(void)
|
|||
assert(
|
||||
strcmp(
|
||||
schema_version,
|
||||
"13"
|
||||
"14"
|
||||
) == 0
|
||||
);
|
||||
|
||||
|
|
@ -1412,7 +1413,17 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
|
|||
assert(database_initialize(path, "Migration V13 synthétique", directory));
|
||||
assert(sqlite3_open(path, &sqlite_database) == SQLITE_OK);
|
||||
test_database_execute_sql(sqlite_database,
|
||||
"INSERT INTO entites(id,type_id,valeur,label,confiance,created_at,"
|
||||
"updated_at,status) VALUES("
|
||||
"'20000000-0000-4000-8000-000000000014',"
|
||||
"(SELECT id FROM types_entite WHERE code='person'),"
|
||||
"'Personne historique','Personne historique',50,"
|
||||
"'2026-07-28T08:00:00Z','2026-07-28T08:00:00Z','active');"
|
||||
"INSERT INTO person_roles(entity_id,role,updated_at) VALUES("
|
||||
"'20000000-0000-4000-8000-000000000014','alleged_scammer',"
|
||||
"'2026-07-28T08:00:00Z');"
|
||||
"DROP TABLE preuve_entite_sources;"
|
||||
"DROP TABLE person_role_assignments;"
|
||||
"UPDATE metadata SET value='12' WHERE key='schema_version';"
|
||||
"INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,"
|
||||
"imported_at,updated_at,status,locked,original_name) VALUES("
|
||||
|
|
@ -1437,11 +1448,17 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
|
|||
legacy_sources = test_database_read_single_text(sqlite_database,
|
||||
"SELECT COUNT(*) FROM preuve_entite_sources "
|
||||
"WHERE source_kind='legacy_manual';");
|
||||
assert(strcmp(version, "13") == 0);
|
||||
assert(strcmp(version, "14") == 0);
|
||||
assert(strcmp(legacy_sources, "1") == 0);
|
||||
char *legacy_role = test_database_read_single_text(sqlite_database,
|
||||
"SELECT role_code || ':' || provenance_kind "
|
||||
"FROM person_role_assignments WHERE entity_id="
|
||||
"'20000000-0000-4000-8000-000000000014';");
|
||||
assert(strcmp(legacy_role, "alleged_scammer:legacy_manual") == 0);
|
||||
assert(sqlite3_close(sqlite_database) == SQLITE_OK);
|
||||
assert(g_remove(path) == 0 && g_rmdir(directory) == 0);
|
||||
g_free(legacy_sources);
|
||||
g_free(legacy_role);
|
||||
g_free(version);
|
||||
g_free(path);
|
||||
g_free(directory);
|
||||
|
|
|
|||
122
tests/test_evidence_preview.c
Normal file
122
tests/test_evidence_preview.c
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#include "core/evidence_preview.h"
|
||||
#include "core/file_hash.h"
|
||||
#include <cairo.h>
|
||||
#include <stdio.h>
|
||||
#include <jpeglib.h>
|
||||
#include "core/evidence_preview_task.h"
|
||||
#include <glib/gstdio.h>
|
||||
|
||||
static void test_preview_png_and_guards(void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
char *root = g_dir_make_tmp("labfy-preview-test-XXXXXX", &error);
|
||||
char *path = g_build_filename(root, "SPECIMEN.png", NULL);
|
||||
cairo_surface_t *fixture = cairo_image_surface_create(
|
||||
CAIRO_FORMAT_RGB24, 32, 20);
|
||||
cairo_t *cr = cairo_create(fixture);
|
||||
char *sha256 = NULL;
|
||||
guint64 size_bytes = 0;
|
||||
EvidencePreviewRequest *request;
|
||||
EvidencePreviewResult *result;
|
||||
g_assert_nonnull(root);
|
||||
cairo_set_source_rgb(cr, 0.2, 0.4, 0.6);
|
||||
cairo_paint(cr);
|
||||
g_assert_cmpint(cairo_surface_write_to_png(fixture, path), ==,
|
||||
CAIRO_STATUS_SUCCESS);
|
||||
g_assert_true(file_hash_compute_sha256(
|
||||
path, NULL, &sha256, &size_bytes, &error));
|
||||
request = evidence_preview_request_new(root,
|
||||
"10000000-0000-4000-8000-000000000109", "SPECIMEN.png",
|
||||
sha256, "image/png", 7);
|
||||
result = evidence_preview_load(request, NULL, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_nonnull(result);
|
||||
g_assert_true(evidence_preview_result_matches(result,
|
||||
"10000000-0000-4000-8000-000000000109", 7));
|
||||
g_assert_false(evidence_preview_result_matches(result,
|
||||
"10000000-0000-4000-8000-000000000109", 8));
|
||||
evidence_preview_result_free(result);
|
||||
g_free(request->expected_sha256);
|
||||
request->expected_sha256 = g_strdup(
|
||||
"0000000000000000000000000000000000000000000000000000000000000000");
|
||||
result = evidence_preview_load(request, NULL, &error);
|
||||
g_assert_null(result);
|
||||
g_assert_error(error, g_quark_from_static_string(
|
||||
"evidence-preview-error"), 2);
|
||||
g_clear_error(&error);
|
||||
evidence_preview_request_free(request);
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(fixture);
|
||||
g_assert_cmpint(g_remove(path), ==, 0);
|
||||
g_assert_cmpint(g_rmdir(root), ==, 0);
|
||||
g_free(sha256); g_free(path); g_free(root);
|
||||
}
|
||||
|
||||
static void write_jpeg(const char *path)
|
||||
{
|
||||
struct jpeg_compress_struct compressor;
|
||||
struct jpeg_error_mgr errors;
|
||||
FILE *output = fopen(path, "wb");
|
||||
JSAMPLE row[3 * 32];
|
||||
g_assert_nonnull(output);
|
||||
memset(row, 0x80, sizeof(row));
|
||||
compressor.err = jpeg_std_error(&errors);
|
||||
jpeg_create_compress(&compressor);
|
||||
jpeg_stdio_dest(&compressor, output);
|
||||
compressor.image_width = 32; compressor.image_height = 20;
|
||||
compressor.input_components = 3; compressor.in_color_space = JCS_RGB;
|
||||
jpeg_set_defaults(&compressor); jpeg_start_compress(&compressor, TRUE);
|
||||
while (compressor.next_scanline < compressor.image_height) {
|
||||
JSAMPROW rows[] = {row};
|
||||
jpeg_write_scanlines(&compressor, rows, 1);
|
||||
}
|
||||
jpeg_finish_compress(&compressor); jpeg_destroy_compress(&compressor);
|
||||
fclose(output);
|
||||
}
|
||||
|
||||
typedef struct { GMainLoop *loop; gboolean completed; } AsyncState;
|
||||
static void preview_done(BackgroundTask *task, gpointer data)
|
||||
{
|
||||
AsyncState *state = data;
|
||||
state->completed = background_task_get_state(task) ==
|
||||
BACKGROUND_TASK_STATE_COMPLETED;
|
||||
g_main_loop_quit(state->loop);
|
||||
}
|
||||
static void test_preview_jpeg_async(void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
char *root = g_dir_make_tmp("labfy-preview-jpeg-XXXXXX", &error);
|
||||
char *path = g_build_filename(root, "SPECIMEN.jpg", NULL);
|
||||
char *sha = NULL; guint64 size = 0;
|
||||
TaskManager *manager = task_manager_new();
|
||||
AsyncState state = {.loop = g_main_loop_new(NULL, FALSE)};
|
||||
write_jpeg(path);
|
||||
g_assert_true(file_hash_compute_sha256(path, NULL, &sha, &size, &error));
|
||||
EvidencePreviewRequest *request = evidence_preview_request_new(root,
|
||||
"10000000-0000-4000-8000-000000000110", "SPECIMEN.jpg",
|
||||
sha, "image/jpeg", 9);
|
||||
BackgroundTask *task = evidence_preview_task_start(manager, request,
|
||||
preview_done, &state, NULL, &error);
|
||||
g_assert_nonnull(task);
|
||||
g_main_loop_run(state.loop);
|
||||
g_assert_true(state.completed);
|
||||
EvidencePreviewResult *result = background_task_get_result(task);
|
||||
g_assert_nonnull(result);
|
||||
g_assert_cmpint(result->width, ==, 32);
|
||||
g_assert_cmpint(result->height, ==, 20);
|
||||
background_task_unref(task);
|
||||
evidence_preview_request_free(request);
|
||||
task_manager_free(manager); g_main_loop_unref(state.loop);
|
||||
g_assert_cmpint(g_remove(path), ==, 0); g_assert_cmpint(g_rmdir(root), ==, 0);
|
||||
g_free(sha); g_free(path); g_free(root);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
g_test_add_func("/evidence-preview/png-and-guards",
|
||||
test_preview_png_and_guards);
|
||||
g_test_add_func("/evidence-preview/jpeg-async",
|
||||
test_preview_jpeg_async);
|
||||
return g_test_run();
|
||||
}
|
||||
109
tests/test_evidence_selection_model.c
Normal file
109
tests/test_evidence_selection_model.c
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#include "models/evidence_selection_model.h"
|
||||
#include <glib.h>
|
||||
|
||||
static EvidenceRecord *record(const char *id, const char *name,
|
||||
const char *type, const char *label, const char *description)
|
||||
{
|
||||
GError *error = NULL;
|
||||
EvidenceRecord *r = evidence_record_new(id, name, "synthetic.bin",
|
||||
"01_Preuves_Originales/synthetic.bin", type, 12,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"2026-07-28T10:00:00Z", NULL, NULL, description,
|
||||
EVIDENCE_INTEGRITY_STATUS_UNKNOWN, &error);
|
||||
g_assert_no_error(error);
|
||||
evidence_record_set_display_metadata(r, label, "image/png");
|
||||
return r;
|
||||
}
|
||||
static void test_search_filter_selection(void)
|
||||
{
|
||||
GPtrArray *records = g_ptr_array_new_with_free_func(
|
||||
(GDestroyNotify) evidence_record_free);
|
||||
g_ptr_array_add(records, record(
|
||||
"10000000-0000-4000-8000-000000000001", "SPECIMEN Alpha.PNG",
|
||||
"screenshot", "Capture d'écran", "Écran de démonstration"));
|
||||
g_ptr_array_add(records, record(
|
||||
"10000000-0000-4000-8000-000000000002", "specimen-beta.jpg",
|
||||
"photo", "Photographie", "Portrait entièrement synthétique"));
|
||||
EvidenceSelectionModel *model = evidence_selection_model_new(records);
|
||||
GPtrArray *visible = evidence_selection_model_list_visible(model);
|
||||
g_assert_cmpuint(visible->len, ==, 2); g_ptr_array_unref(visible);
|
||||
g_assert_true(evidence_selection_model_select(model,
|
||||
"10000000-0000-4000-8000-000000000001"));
|
||||
evidence_selection_model_set_query(model, "ÉCRAN");
|
||||
visible = evidence_selection_model_list_visible(model);
|
||||
g_assert_cmpuint(visible->len, ==, 1); g_ptr_array_unref(visible);
|
||||
g_assert_nonnull(evidence_selection_model_get_selected(model));
|
||||
evidence_selection_model_set_type(model, "photo");
|
||||
g_assert_null(evidence_selection_model_get_selected(model));
|
||||
visible = evidence_selection_model_list_visible(model);
|
||||
g_assert_cmpuint(visible->len, ==, 0); g_ptr_array_unref(visible);
|
||||
evidence_selection_model_set_query(model, "");
|
||||
visible = evidence_selection_model_list_visible(model);
|
||||
g_assert_cmpuint(visible->len, ==, 1); g_ptr_array_unref(visible);
|
||||
evidence_selection_model_set_type(model, NULL);
|
||||
evidence_selection_model_set_query(model, "capture");
|
||||
visible = evidence_selection_model_list_visible(model);
|
||||
g_assert_cmpuint(visible->len, ==, 1); g_ptr_array_unref(visible);
|
||||
evidence_selection_model_free(model);
|
||||
g_ptr_array_unref(records);
|
||||
}
|
||||
|
||||
static void test_owned_records_survive_source(void)
|
||||
{
|
||||
GPtrArray *records = g_ptr_array_new_with_free_func(
|
||||
(GDestroyNotify) evidence_record_free);
|
||||
g_ptr_array_add(records, record(
|
||||
"10000000-0000-4000-8000-000000000003",
|
||||
"SPECIMEN durée de vie.png", "screenshot",
|
||||
"Capture d’écran", NULL));
|
||||
EvidenceSelectionModel *model = evidence_selection_model_new(records);
|
||||
g_ptr_array_unref(records);
|
||||
evidence_selection_model_set_query(model, "d");
|
||||
GPtrArray *visible = evidence_selection_model_list_visible(model);
|
||||
g_assert_cmpuint(visible->len, ==, 1);
|
||||
g_ptr_array_unref(visible);
|
||||
evidence_selection_model_free(model);
|
||||
}
|
||||
|
||||
static void test_null_utf8_and_successive_queries(void)
|
||||
{
|
||||
GPtrArray *records = g_ptr_array_new_with_free_func(
|
||||
(GDestroyNotify) evidence_record_free);
|
||||
EvidenceRecord *without_optional = record(
|
||||
"10000000-0000-4000-8000-000000000004",
|
||||
"SPECIMEN été.png", "screenshot", "Capture française", NULL);
|
||||
evidence_record_set_display_metadata(
|
||||
without_optional, "Capture française", NULL);
|
||||
g_ptr_array_add(records, without_optional);
|
||||
EvidenceSelectionModel *model = evidence_selection_model_new(records);
|
||||
const char *queries[] = {
|
||||
NULL, "", "é", "ÉTÉ", "capture", "screenshot",
|
||||
"image/png", "不存在", "a", "ab", "abc", NULL
|
||||
};
|
||||
for (guint i = 0; i < G_N_ELEMENTS(queries); i++) {
|
||||
evidence_selection_model_set_query(model, queries[i]);
|
||||
GPtrArray *visible = evidence_selection_model_list_visible(model);
|
||||
g_assert_nonnull(visible);
|
||||
g_ptr_array_unref(visible);
|
||||
}
|
||||
char *long_query = g_strnfill(4096, 'x');
|
||||
evidence_selection_model_set_query(model, long_query);
|
||||
GPtrArray *visible = evidence_selection_model_list_visible(model);
|
||||
g_assert_cmpuint(visible->len, ==, 0);
|
||||
g_ptr_array_unref(visible);
|
||||
g_free(long_query);
|
||||
evidence_selection_model_free(model);
|
||||
g_ptr_array_unref(records);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
g_test_add_func("/evidence-selection/search-filter",
|
||||
test_search_filter_selection);
|
||||
g_test_add_func("/evidence-selection/owned-records",
|
||||
test_owned_records_survive_source);
|
||||
g_test_add_func("/evidence-selection/null-utf8-successive",
|
||||
test_null_utf8_and_successive_queries);
|
||||
return g_test_run();
|
||||
}
|
||||
60
tests/test_person_confirmation_summary.c
Normal file
60
tests/test_person_confirmation_summary.c
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#include "core/person_confirmation_summary.h"
|
||||
#include <glib.h>
|
||||
|
||||
static EvidenceRecord *record(void)
|
||||
{
|
||||
EvidenceRecord *evidence = evidence_record_new(
|
||||
"10000000-0000-4000-8000-000000000010",
|
||||
"SPECIMEN-confirmation.png", "specimen.png",
|
||||
"01_Preuves_Originales/specimen.png", "screenshot", 42,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"2026-07-28T10:00:00Z", NULL, NULL, "Description SPECIMEN",
|
||||
EVIDENCE_INTEGRITY_STATUS_VALID, NULL);
|
||||
evidence_record_set_display_metadata(
|
||||
evidence, "Capture d’écran", "image/png");
|
||||
return evidence;
|
||||
}
|
||||
|
||||
static void test_full_summary(void)
|
||||
{
|
||||
EvidenceRecord *evidence = record();
|
||||
GPtrArray *roles = g_ptr_array_new();
|
||||
g_ptr_array_add(roles, "Victime");
|
||||
g_ptr_array_add(roles, "Témoin");
|
||||
char *summary = person_confirmation_summary_build(
|
||||
"Personne SPECIMEN", "Nom SPECIMEN", "Pseudo", "Présumé", 55,
|
||||
"Notes synthétiques", roles, evidence);
|
||||
const char *values[] = {
|
||||
"Personne SPECIMEN", "Nom SPECIMEN", "Pseudo", "Présumé", "55 %",
|
||||
"Victime", "Témoin", "SPECIMEN-confirmation.png",
|
||||
"Capture d’écran", "image/png", "42", "2026-07-28T10:00:00Z",
|
||||
"aaaaaaaaaaaa…", "Valide", "Description SPECIMEN",
|
||||
"Aucune écriture n’a encore été effectuée"
|
||||
};
|
||||
for (guint i = 0; i < G_N_ELEMENTS(values); i++)
|
||||
g_assert_nonnull(strstr(summary, values[i]));
|
||||
g_free(summary);
|
||||
g_ptr_array_unref(roles);
|
||||
evidence_record_free(evidence);
|
||||
}
|
||||
|
||||
static void test_optional_summary(void)
|
||||
{
|
||||
char *summary = person_confirmation_summary_build(
|
||||
"SPECIMEN", "", NULL, "Inconnu", 0, "", NULL, NULL);
|
||||
g_assert_nonnull(strstr(summary, "Nom déclaré : Non renseigné"));
|
||||
g_assert_nonnull(strstr(summary, "Pseudonyme : Non renseigné"));
|
||||
g_assert_nonnull(strstr(summary, "Notes factuelles : Aucune"));
|
||||
g_assert_nonnull(strstr(summary, "Aucun rôle sélectionné"));
|
||||
g_assert_nonnull(strstr(summary,
|
||||
"Aucune preuve principale associée"));
|
||||
g_free(summary);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
g_test_add_func("/person-confirmation/full", test_full_summary);
|
||||
g_test_add_func("/person-confirmation/optional", test_optional_summary);
|
||||
return g_test_run();
|
||||
}
|
||||
24
tests/test_person_creation_guard.c
Normal file
24
tests/test_person_creation_guard.c
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#include "core/person_creation_guard.h"
|
||||
#include <glib.h>
|
||||
static void test_guard(void)
|
||||
{
|
||||
PersonCreationGuard *guard = person_creation_guard_new(3,
|
||||
"/tmp/synthetic-a", "/tmp/synthetic-a/Enquete.sqlite");
|
||||
g_assert_true(person_creation_guard_matches(guard, TRUE, 3,
|
||||
"/tmp/synthetic-a", "/tmp/synthetic-a/Enquete.sqlite"));
|
||||
g_assert_false(person_creation_guard_matches(guard, TRUE, 4,
|
||||
"/tmp/synthetic-a", "/tmp/synthetic-a/Enquete.sqlite"));
|
||||
g_assert_false(person_creation_guard_matches(guard, TRUE, 3,
|
||||
"/tmp/synthetic-a", "/tmp/synthetic-b/Enquete.sqlite"));
|
||||
g_assert_false(person_creation_guard_matches(guard, FALSE, 3,
|
||||
"/tmp/synthetic-a", "/tmp/synthetic-a/Enquete.sqlite"));
|
||||
g_assert_false(person_creation_guard_matches(guard, TRUE, 3,
|
||||
NULL, NULL));
|
||||
person_creation_guard_free(guard);
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
g_test_add_func("/person-creation-guard/session", test_guard);
|
||||
return g_test_run();
|
||||
}
|
||||
117
tests/test_person_dialog_lifecycle.c
Normal file
117
tests/test_person_dialog_lifecycle.c
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
#include "core/person_dialog_lifecycle.h"
|
||||
#include <glib.h>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
guint callback_count;
|
||||
guint person_count;
|
||||
guint role_count;
|
||||
guint attachment_count;
|
||||
guint manual_source_count;
|
||||
guint graph_refresh_count;
|
||||
guint quit_count;
|
||||
gboolean main_window_active;
|
||||
} CancellationEffects;
|
||||
|
||||
static void cancel_once(PersonDialogLifecycle *lifecycle,
|
||||
CancellationEffects *effects)
|
||||
{
|
||||
if (!person_dialog_lifecycle_cancel(lifecycle)) return;
|
||||
effects->callback_count++;
|
||||
}
|
||||
|
||||
static void assert_no_business_effect(const CancellationEffects *effects)
|
||||
{
|
||||
g_assert_cmpuint(effects->person_count, ==, 0);
|
||||
g_assert_cmpuint(effects->role_count, ==, 0);
|
||||
g_assert_cmpuint(effects->attachment_count, ==, 0);
|
||||
g_assert_cmpuint(effects->manual_source_count, ==, 0);
|
||||
g_assert_cmpuint(effects->graph_refresh_count, ==, 0);
|
||||
g_assert_cmpuint(effects->quit_count, ==, 0);
|
||||
g_assert_true(effects->main_window_active);
|
||||
}
|
||||
|
||||
static void test_cancel_on_each_step(void)
|
||||
{
|
||||
for (guint step = 0; step < 4; step++) {
|
||||
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
|
||||
CancellationEffects effects = {.main_window_active = TRUE};
|
||||
cancel_once(lifecycle, &effects);
|
||||
g_assert_true(person_dialog_lifecycle_is_finished(lifecycle));
|
||||
g_assert_true(person_dialog_lifecycle_is_cancelled(lifecycle));
|
||||
g_assert_cmpuint(effects.callback_count, ==, 1);
|
||||
assert_no_business_effect(&effects);
|
||||
person_dialog_lifecycle_free(lifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_close_and_double_cancel_are_idempotent(void)
|
||||
{
|
||||
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
|
||||
CancellationEffects effects = {.main_window_active = TRUE};
|
||||
cancel_once(lifecycle, &effects);
|
||||
cancel_once(lifecycle, &effects);
|
||||
g_assert_false(person_dialog_lifecycle_cancel(lifecycle));
|
||||
g_assert_false(person_dialog_lifecycle_complete(lifecycle));
|
||||
g_assert_cmpuint(effects.callback_count, ==, 1);
|
||||
assert_no_business_effect(&effects);
|
||||
person_dialog_lifecycle_free(lifecycle);
|
||||
}
|
||||
|
||||
static void test_preview_is_cancelled_and_late_result_ignored(void)
|
||||
{
|
||||
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
|
||||
CancellationEffects effects = {.main_window_active = TRUE};
|
||||
guint64 generation =
|
||||
person_dialog_lifecycle_begin_preview(lifecycle);
|
||||
g_assert_true(person_dialog_lifecycle_has_preview(lifecycle));
|
||||
g_assert_true(person_dialog_lifecycle_accepts_preview(
|
||||
lifecycle, generation));
|
||||
cancel_once(lifecycle, &effects);
|
||||
g_assert_false(person_dialog_lifecycle_has_preview(lifecycle));
|
||||
g_assert_false(person_dialog_lifecycle_accepts_preview(
|
||||
lifecycle, generation));
|
||||
g_assert_cmpuint(person_dialog_lifecycle_get_generation(lifecycle),
|
||||
>, generation);
|
||||
assert_no_business_effect(&effects);
|
||||
person_dialog_lifecycle_free(lifecycle);
|
||||
}
|
||||
|
||||
static void test_complete_once(void)
|
||||
{
|
||||
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
|
||||
g_assert_true(person_dialog_lifecycle_complete(lifecycle));
|
||||
g_assert_true(person_dialog_lifecycle_is_finished(lifecycle));
|
||||
g_assert_false(person_dialog_lifecycle_is_cancelled(lifecycle));
|
||||
g_assert_false(person_dialog_lifecycle_complete(lifecycle));
|
||||
g_assert_false(person_dialog_lifecycle_cancel(lifecycle));
|
||||
person_dialog_lifecycle_free(lifecycle);
|
||||
}
|
||||
|
||||
static void test_null_and_full_destruction(void)
|
||||
{
|
||||
g_assert_false(person_dialog_lifecycle_cancel(NULL));
|
||||
g_assert_false(person_dialog_lifecycle_complete(NULL));
|
||||
g_assert_false(person_dialog_lifecycle_accepts_preview(NULL, 1));
|
||||
person_dialog_lifecycle_free(NULL);
|
||||
for (guint i = 0; i < 128; i++) {
|
||||
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
|
||||
person_dialog_lifecycle_begin_preview(lifecycle);
|
||||
person_dialog_lifecycle_cancel(lifecycle);
|
||||
person_dialog_lifecycle_free(lifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
g_test_add_func("/person-dialog/cancel-each-step",
|
||||
test_cancel_on_each_step);
|
||||
g_test_add_func("/person-dialog/close-idempotent",
|
||||
test_close_and_double_cancel_are_idempotent);
|
||||
g_test_add_func("/person-dialog/late-preview",
|
||||
test_preview_is_cancelled_and_late_result_ignored);
|
||||
g_test_add_func("/person-dialog/complete-once", test_complete_once);
|
||||
g_test_add_func("/person-dialog/destruction", test_null_and_full_destruction);
|
||||
return g_test_run();
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
#include "core/person_entity_service.h"
|
||||
#include "database/database.h"
|
||||
#include "dao/entity_dao.h"
|
||||
#include "dao/person_role_assignment_dao.h"
|
||||
#include "models/entity_record.h"
|
||||
#include "database/statement.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
|
@ -25,6 +27,15 @@ static void test_person_entity_create(void)
|
|||
.identification_status = "suspected",
|
||||
.notes = "Identité non confirmée.", .confidence = 30,
|
||||
.evidence_identifier = NULL};
|
||||
PersonRoleAssignmentInput victim = {
|
||||
.role_code = "victim", .provenance_kind = "manual",
|
||||
.has_confidence = TRUE, .confidence = 70};
|
||||
PersonRoleAssignmentInput witness = {
|
||||
.role_code = "witness", .provenance_kind = "manual"};
|
||||
GPtrArray *roles = g_ptr_array_new();
|
||||
g_ptr_array_add(roles, &victim);
|
||||
g_ptr_array_add(roles, &witness);
|
||||
input.role_assignments = roles;
|
||||
directory = g_dir_make_tmp("labfy-person-service-test-XXXXXX", &error);
|
||||
assert(directory != NULL && error == NULL);
|
||||
path = g_build_filename(directory, "Enquete.sqlite", NULL);
|
||||
|
|
@ -37,6 +48,13 @@ static void test_person_entity_create(void)
|
|||
assert(record != NULL && error == NULL);
|
||||
assert(strcmp(entity_record_get_type_identifier(record), "person") == 0);
|
||||
assert(entity_record_get_confidence(record) == 30);
|
||||
PersonRoleAssignmentDao *role_dao =
|
||||
person_role_assignment_dao_new(database, &error);
|
||||
GPtrArray *saved_roles = person_role_assignment_dao_list_by_entity(
|
||||
role_dao, identifier, &error);
|
||||
assert(saved_roles != NULL && saved_roles->len == 2);
|
||||
g_ptr_array_unref(saved_roles);
|
||||
person_role_assignment_dao_free(role_dao);
|
||||
assert(entity_record_get_person_role(record) == PERSON_ROLE_UNCATEGORIZED);
|
||||
entity_record_free(record); record = NULL;
|
||||
assert(person_entity_service_update_role(database, identifier,
|
||||
|
|
@ -63,10 +81,72 @@ static void test_person_entity_create(void)
|
|||
assert(strcmp(entity_record_get_label(record), "Vraie identité") == 0);
|
||||
entity_record_free(record); entity_dao_free(dao); database_close(database);
|
||||
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
|
||||
g_ptr_array_unref(roles);
|
||||
g_free(identifier); g_free(path); g_free(directory);
|
||||
}
|
||||
|
||||
static gint64 count_rows(Database *database, const char *sql)
|
||||
{
|
||||
DatabaseStatement *statement = database_statement_prepare(database, sql);
|
||||
int64_t count = -1;
|
||||
assert(statement != NULL);
|
||||
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
|
||||
assert(database_statement_column_int64(statement, 0, &count));
|
||||
database_statement_finalize(statement);
|
||||
return count;
|
||||
}
|
||||
|
||||
static void test_person_entity_rollbacks(void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
char *directory = g_dir_make_tmp("labfy-person-rollback-XXXXXX", &error);
|
||||
char *path = g_build_filename(directory, "Enquete.sqlite", NULL);
|
||||
Database *database;
|
||||
PersonRoleAssignmentInput first = {
|
||||
.role_code="victim", .provenance_kind="manual"};
|
||||
PersonRoleAssignmentInput duplicate = {
|
||||
.role_code="victim", .provenance_kind="manual"};
|
||||
GPtrArray *roles = g_ptr_array_new();
|
||||
PersonEntityInput input = {
|
||||
.designation="Personne rollback SPECIMEN",
|
||||
.identification_status="unknown", .confidence=20,
|
||||
.role_assignments=roles};
|
||||
g_assert_true(database_initialize(path, "Rollback synthétique", directory));
|
||||
database = database_open(path);
|
||||
g_assert_true(database_migrate_to_latest(database));
|
||||
g_ptr_array_add(roles, &first); g_ptr_array_add(roles, &duplicate);
|
||||
g_assert_false(person_entity_service_create(
|
||||
database, &input, NULL, &error));
|
||||
g_clear_error(&error);
|
||||
g_assert_cmpint(count_rows(database,
|
||||
"SELECT COUNT(*) FROM entites WHERE label='Personne rollback SPECIMEN';"),
|
||||
==, 0);
|
||||
g_assert_cmpint(count_rows(database,
|
||||
"SELECT COUNT(*) FROM person_role_assignments;"), ==, 0);
|
||||
g_ptr_array_set_size(roles, 1);
|
||||
input.evidence_identifier =
|
||||
"10000000-0000-4000-8000-000000000999";
|
||||
first.evidence_identifier = NULL;
|
||||
g_assert_false(person_entity_service_create(
|
||||
database, &input, NULL, &error));
|
||||
g_clear_error(&error);
|
||||
g_assert_cmpint(count_rows(database,
|
||||
"SELECT COUNT(*) FROM entites WHERE label='Personne rollback SPECIMEN';"),
|
||||
==, 0);
|
||||
g_assert_cmpint(count_rows(database,
|
||||
"SELECT COUNT(*) FROM person_role_assignments;"), ==, 0);
|
||||
g_assert_cmpint(count_rows(database,
|
||||
"SELECT COUNT(*) FROM preuve_entites;"), ==, 0);
|
||||
g_assert_cmpint(count_rows(database,
|
||||
"SELECT COUNT(*) FROM preuve_entite_sources;"), ==, 0);
|
||||
g_ptr_array_unref(roles); database_close(database);
|
||||
g_assert_cmpint(g_remove(path), ==, 0);
|
||||
g_assert_cmpint(g_rmdir(directory), ==, 0);
|
||||
g_free(path); g_free(directory);
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
test_person_entity_create();
|
||||
test_person_entity_rollbacks();
|
||||
puts("PersonEntityService : tous les tests sont valides."); return 0;
|
||||
}
|
||||
|
|
|
|||
113
tests/test_person_role_assignment_dao.c
Normal file
113
tests/test_person_role_assignment_dao.c
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#include "dao/person_role_assignment_dao.h"
|
||||
#include "dao/entity_dao.h"
|
||||
#include "dao/evidence_dao.h"
|
||||
#include "database/transaction.h"
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
|
||||
static EntityRecord *make_person(const char *id)
|
||||
{
|
||||
GError *error = NULL;
|
||||
EntityRecord *r = entity_record_new(id, "person", id,
|
||||
"Personne SPECIMEN", NULL, 50, "2026-07-28T10:00:00Z",
|
||||
"2026-07-28T10:00:00Z", ENTITY_STATUS_ACTIVE, &error);
|
||||
g_assert_no_error(error); return r;
|
||||
}
|
||||
static EvidenceRecord *make_evidence(const char *id, const char *name)
|
||||
{
|
||||
GError *error = NULL;
|
||||
EvidenceRecord *r = evidence_record_new(id, name, name,
|
||||
name, "document", 1,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"2026-07-28T10:00:00Z", NULL, NULL, NULL,
|
||||
EVIDENCE_INTEGRITY_STATUS_UNKNOWN, &error);
|
||||
g_assert_no_error(error); return r;
|
||||
}
|
||||
static void test_dao_roles(void)
|
||||
{
|
||||
GError *error = NULL;
|
||||
char *dir = g_dir_make_tmp("labfy-role-dao-XXXXXX", &error);
|
||||
char *path = g_build_filename(dir, "Enquete.sqlite", NULL);
|
||||
Database *db; EntityDao *entities; EvidenceDao *evidences;
|
||||
PersonRoleAssignmentDao *roles; EntityRecord *person;
|
||||
EvidenceRecord *evidence;
|
||||
const char *person_id = "20000000-0000-4000-8000-000000000109";
|
||||
const char *evidence_id = "10000000-0000-4000-8000-000000000109";
|
||||
const char *evidence_id_2 = "10000000-0000-4000-8000-000000000110";
|
||||
g_assert_true(database_initialize(path, "Rôles synthétiques", dir));
|
||||
db = database_open(path); g_assert_true(database_migrate_to_latest(db));
|
||||
entities = entity_dao_new(db, &error); evidences = evidence_dao_new(db, &error);
|
||||
roles = person_role_assignment_dao_new(db, &error);
|
||||
person = make_person(person_id); g_assert_true(entity_dao_insert(entities, person, &error));
|
||||
evidence = make_evidence(evidence_id, "specimen.bin");
|
||||
g_assert_true(evidence_dao_insert(evidences, evidence, &error));
|
||||
EvidenceRecord *evidence_2 = make_evidence(
|
||||
evidence_id_2, "specimen-2.bin");
|
||||
g_assert_true(evidence_dao_insert(evidences, evidence_2, &error));
|
||||
PersonRoleAssignmentInput without = {
|
||||
.role_code="victim", .provenance_kind="manual",
|
||||
.has_confidence=TRUE, .confidence=0};
|
||||
PersonRoleAssignmentInput with = {
|
||||
.role_code="witness", .evidence_identifier=(char *) evidence_id,
|
||||
.provenance_kind="manual", .has_confidence=TRUE, .confidence=100};
|
||||
g_assert_true(person_role_assignment_dao_insert(
|
||||
roles, person_id, &without, &error));
|
||||
g_assert_true(person_role_assignment_dao_insert(
|
||||
roles, person_id, &with, &error));
|
||||
PersonRoleAssignmentInput same_other_proof = with;
|
||||
same_other_proof.evidence_identifier = (char *) evidence_id_2;
|
||||
g_assert_true(person_role_assignment_dao_insert(
|
||||
roles, person_id, &same_other_proof, &error));
|
||||
GPtrArray *saved = person_role_assignment_dao_list_by_entity(
|
||||
roles, person_id, &error);
|
||||
g_assert_cmpuint(saved->len, ==, 3);
|
||||
g_ptr_array_unref(saved);
|
||||
PersonRoleAssignmentInput invalid = {
|
||||
.role_code="unknown-code", .provenance_kind="manual"};
|
||||
g_assert_false(person_role_assignment_dao_insert(
|
||||
roles, person_id, &invalid, &error)); g_clear_error(&error);
|
||||
invalid.role_code="victim"; invalid.has_confidence=TRUE; invalid.confidence=-1;
|
||||
g_assert_false(person_role_assignment_dao_insert(
|
||||
roles, person_id, &invalid, &error)); g_clear_error(&error);
|
||||
invalid.confidence=101;
|
||||
g_assert_false(person_role_assignment_dao_insert(
|
||||
roles, person_id, &invalid, &error)); g_clear_error(&error);
|
||||
with.evidence_identifier="10000000-0000-4000-8000-000000000999";
|
||||
g_assert_false(person_role_assignment_dao_insert(
|
||||
roles, person_id, &with, &error)); g_clear_error(&error);
|
||||
with.evidence_identifier=(char *) evidence_id;
|
||||
g_assert_false(person_role_assignment_dao_insert(roles,
|
||||
"20000000-0000-4000-8000-000000000999", &with, &error));
|
||||
g_clear_error(&error);
|
||||
const char *rollback_person =
|
||||
"20000000-0000-4000-8000-000000000110";
|
||||
EntityRecord *person_2 = make_person(rollback_person);
|
||||
g_assert_true(entity_dao_insert(entities, person_2, &error));
|
||||
GPtrArray *batch = g_ptr_array_new();
|
||||
PersonRoleAssignmentInput batch_valid = {
|
||||
.role_code="intermediary", .provenance_kind="manual"};
|
||||
PersonRoleAssignmentInput batch_invalid = {
|
||||
.role_code="not-known", .provenance_kind="manual"};
|
||||
g_ptr_array_add(batch, &batch_valid); g_ptr_array_add(batch, &batch_invalid);
|
||||
g_assert_true(database_transaction_begin(db));
|
||||
g_assert_false(person_role_assignment_dao_insert_all(
|
||||
roles, rollback_person, batch, &error));
|
||||
g_clear_error(&error);
|
||||
g_assert_true(database_transaction_rollback(db));
|
||||
saved = person_role_assignment_dao_list_by_entity(
|
||||
roles, rollback_person, &error);
|
||||
g_assert_cmpuint(saved->len, ==, 0);
|
||||
g_ptr_array_unref(saved); g_ptr_array_unref(batch);
|
||||
evidence_record_free(evidence_2); entity_record_free(person_2);
|
||||
evidence_record_free(evidence); entity_record_free(person);
|
||||
person_role_assignment_dao_free(roles); evidence_dao_free(evidences);
|
||||
entity_dao_free(entities); database_close(db);
|
||||
g_assert_cmpint(g_remove(path), ==, 0); g_assert_cmpint(g_rmdir(dir), ==, 0);
|
||||
g_free(path); g_free(dir);
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
g_test_add_func("/person-role-assignment-dao/direct", test_dao_roles);
|
||||
return g_test_run();
|
||||
}
|
||||
Loading…
Reference in a new issue