2148 lines
53 KiB
Markdown
2148 lines
53 KiB
Markdown
# Endobest Field Mapping Configuration Guide
|
|
|
|
## Part 2: Field Mapping & Configuration
|
|
|
|
**Document Version:** 2.0 (Updated with new module references)
|
|
**Last Updated:** 2025-11-08
|
|
**Audience:** Developers, Business Analysts, Data Managers
|
|
**Language:** English
|
|
|
|
**Note:** Configuration file `Endobest_Dashboard_Config.xlsx` uses `Inclusions_Mapping` sheet for field definitions (see DOCUMENTATION_13_EXCEL_EXPORT.md and DOCUMENTATION_99_CONFIG_GUIDE.md for Excel export configuration)
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Technical Architecture](#technical-architecture)
|
|
3. [Field Processing Logic](#field-processing-logic)
|
|
4. [Configuration File Structure](#configuration-file-structure)
|
|
5. [Column Reference](#column-reference)
|
|
6. [Special Value Prefixes](#special-value-prefixes)
|
|
7. [Data Sources Explained](#data-sources-explained)
|
|
8. [Field Path Syntax](#field-path-syntax)
|
|
9. [Custom Functions Reference](#custom-functions-reference)
|
|
10. [Post-Processing Transformations](#post-processing-transformations)
|
|
11. [Configuration Examples](#configuration-examples)
|
|
12. [User Guide: Adding/Modifying Fields](#user-guide-adding-modifying-fields)
|
|
13. [Common Patterns & Recipes](#common-patterns--recipes)
|
|
14. [Troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The **Field Mapping Configuration** defines which data points are extracted from multiple APIs (RC, GDD, questionnaires) and how they are transformed before export. The configuration is **100% externalized in an Excel file**, enabling non-technical users to add new fields without code modifications.
|
|
|
|
### Key Concepts
|
|
|
|
- **Field Group:** Logical container for related fields (e.g., "Patient_Identification", "Inclusion", "Endotest")
|
|
- **Field Name:** Unique identifier for the field within its group
|
|
- **Source:** Where the data comes from (questionnaire, record, inclusion, request)
|
|
- **Field Path:** JSON path to navigate nested structures
|
|
- **Transformations:** Post-processing rules (labels, templates, conditions)
|
|
- **Custom Functions:** Calculated fields with business logic
|
|
|
|
---
|
|
|
|
## Technical Architecture
|
|
|
|
### Field Extraction Pipeline (Detailed)
|
|
|
|
```
|
|
CONFIGURATION LOADING (startup):
|
|
├─ Load Endobest_Dashboard_Config.xlsx
|
|
├─ Parse Inclusions_Mapping sheet (rows 2 onwards)
|
|
├─ Validate each field configuration
|
|
├─ Parse JSON fields (field_path, value_labels, true_if_any, field_condition)
|
|
└─ Store in DASHBOARD_CONFIG array
|
|
|
|
FIELD PROCESSING (per patient):
|
|
├─ For each field in DASHBOARD_CONFIG:
|
|
│ ├─ Determine source type (questionnaire, record, inclusion, request, calculated)
|
|
│ │
|
|
│ ├─ IF source == questionnaire:
|
|
│ │ ├─ Method: Search by q_id, q_name, or q_category
|
|
│ │ ├─ Data: All questionnaires already fetched for patient
|
|
│ │ ├─ Path: Navigate to field_path within questionnaire answers
|
|
│ │ └─ Result: raw_value or "undefined"
|
|
│ │
|
|
│ ├─ IF source == record:
|
|
│ │ ├─ Data: Patient's clinical record
|
|
│ │ ├─ Path: Navigate JSON structure using field_path
|
|
│ │ └─ Result: raw_value or "undefined"
|
|
│ │
|
|
│ ├─ IF source == inclusion:
|
|
│ │ ├─ Data: Patient inclusion metadata
|
|
│ │ ├─ Path: Navigate nested inclusion structure
|
|
│ │ └─ Result: raw_value or "undefined"
|
|
│ │
|
|
│ ├─ IF source == request:
|
|
│ │ ├─ Data: Lab test request/results
|
|
│ │ ├─ Path: Navigate request JSON structure
|
|
│ │ └─ Result: raw_value or "undefined"
|
|
│ │
|
|
│ ├─ IF source == calculated:
|
|
│ │ ├─ Function: Custom business logic function
|
|
│ │ ├─ Arguments: From field_path
|
|
│ │ ├─ Access: Other fields already processed in output_inclusion
|
|
│ │ └─ Result: Computed value
|
|
│ │
|
|
│ ├─ CHECK field_condition (optional):
|
|
│ │ ├─ If condition is false → Set to "N/A"
|
|
│ │ ├─ If condition is undefined → Set to "undefined"
|
|
│ │ └─ If condition is true → Continue processing
|
|
│ │
|
|
│ ├─ APPLY post-processing transformations:
|
|
│ │ ├─ true_if_any: Convert to boolean
|
|
│ │ ├─ value_labels: Map to localized text
|
|
│ │ ├─ field_template: Apply formatting
|
|
│ │ └─ List joining: Flatten arrays
|
|
│ │
|
|
│ └─ STORE: output_inclusion[field_group][field_name] = final_value
|
|
│
|
|
└─ Result: Complete inclusion with all extended fields
|
|
```
|
|
|
|
### Questionnaire Finding Strategy
|
|
|
|
The system supports **3 methods** to locate questionnaires:
|
|
|
|
```python
|
|
def find_questionnaire(all_questionnaires, source_type, source_value):
|
|
if source_type == "q_id":
|
|
# Direct lookup by questionnaire ID (fastest)
|
|
return all_questionnaires.get(source_value, {}).get("answers")
|
|
|
|
elif source_type == "q_name":
|
|
# Sequential search by questionnaire name
|
|
for qcm_data in all_questionnaires.values():
|
|
if qcm_data["questionnaire"]["name"] == source_value:
|
|
return qcm_data.get("answers")
|
|
return None
|
|
|
|
elif source_type == "q_category":
|
|
# Sequential search by questionnaire category
|
|
for qcm_data in all_questionnaires.values():
|
|
if qcm_data["questionnaire"]["category"] == source_value:
|
|
return qcm_data.get("answers")
|
|
return None
|
|
```
|
|
|
|
**Recommendation:** Use `q_id=` for best performance (direct lookup)
|
|
|
|
### Questionnaire Data Optimization
|
|
|
|
Instead of multiple filtered API calls:
|
|
```
|
|
BEFORE (slow):
|
|
GET /api/surveys/{qcm_id_1}/answers?subject={patient_id}
|
|
GET /api/surveys/{qcm_id_2}/answers?subject={patient_id}
|
|
GET /api/surveys/{qcm_id_3}/answers?subject={patient_id}
|
|
... (N calls per patient)
|
|
|
|
AFTER (optimized - single call):
|
|
POST /api/surveys/filter/with-answers
|
|
payload: {"context": "clinic_research", "subject": patient_id}
|
|
returns: [
|
|
{"questionnaire": {id, name, category}, "answers": {...}},
|
|
{"questionnaire": {id, name, category}, "answers": {...}},
|
|
...
|
|
]
|
|
```
|
|
|
|
All questionnaires are returned in a single call, indexed by ID for fast lookup.
|
|
|
|
---
|
|
|
|
## Field Processing Logic
|
|
|
|
### Step 1: Source Type Determination
|
|
|
|
| Source Prefix | Meaning | Example | Data Location |
|
|
|---------------|---------|---------|----------------|
|
|
| `q_id=` | Questionnaire by ID | `q_id=uuid-123` | `all_questionnaires[uuid-123]["answers"]` |
|
|
| `q_name=` | Questionnaire by name | `q_name=Symptom Check` | Search by `["questionnaire"]["name"]` |
|
|
| `q_category=` | Questionnaire by category | `q_category=Symptoms` | Search by `["questionnaire"]["category"]` |
|
|
| `record` | Clinical record | `record` | `record_data["record"]` |
|
|
| `inclusion` | Inclusion metadata | `inclusion` | `inclusion_data` |
|
|
| `request` | Lab test request | `request` | `request_data` |
|
|
| (Calculated) | Custom function | N/A | Function result |
|
|
|
|
### Step 2: Raw Value Extraction
|
|
|
|
The `field_path` defines how to navigate nested JSON structures:
|
|
|
|
```python
|
|
# Simple path
|
|
field_path = ["patient", "name"]
|
|
# Equivalent to: data["patient"]["name"]
|
|
|
|
# Nested path
|
|
field_path = ["record", "clinicResearchData", 0, "data"]
|
|
# Equivalent to: data["record"]["clinicResearchData"][0]["data"]
|
|
|
|
# Wildcard path (returns array)
|
|
field_path = ["record", "clinicResearchData", "*", "test_name"]
|
|
# Returns: [test_name_1, test_name_2, test_name_3, ...]
|
|
|
|
# Deep wildcard
|
|
field_path = ["record", "*", "results", "*", "value"]
|
|
# Matches all results.*.value across all record items
|
|
```
|
|
|
|
### Step 3: Field Condition Checking (Optional)
|
|
|
|
The `field_condition` allows skipping field processing based on another field's value:
|
|
|
|
```
|
|
IF field_condition is specified:
|
|
├─ Look up condition field value in output_inclusion
|
|
├─ IF condition value is None or "undefined":
|
|
│ └─ Set final_value = "undefined" (skip further processing)
|
|
├─ IF condition value is not a boolean:
|
|
│ └─ Set final_value = "$$$$ Condition Field Error"
|
|
├─ IF condition value is False:
|
|
│ └─ Set final_value = "N/A" (field not applicable)
|
|
└─ IF condition value is True:
|
|
└─ Continue with post-processing
|
|
```
|
|
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_group": "Endotest",
|
|
"field_name": "Request_Status",
|
|
"source_id": "request",
|
|
"field_path": ["status"],
|
|
"field_condition": "Endotest.Request_Sent"
|
|
}
|
|
```
|
|
|
|
Meaning: Only populate "Request_Status" if "Request_Sent" is True. Otherwise set to "N/A".
|
|
|
|
### Step 4: Post-Processing Transformations
|
|
|
|
#### 4a. Array Flattening
|
|
If `raw_value` is an array → Join with `|` delimiter:
|
|
```
|
|
Input: ["Active", "Pending", "Resolved"]
|
|
Output: "Active|Pending|Resolved"
|
|
```
|
|
|
|
#### 4b. Score Dictionary Formatting
|
|
If `raw_value` is dict with keys `['total', 'max']` → Format as string:
|
|
```
|
|
Input: {"total": 8, "max": 10}
|
|
Output: "8/10"
|
|
```
|
|
|
|
#### 4c. true_if_any Transformation
|
|
If `true_if_any` is specified → Convert to boolean:
|
|
```
|
|
true_if_any: ["Active", "Pending"]
|
|
raw_value: "Active"
|
|
|
|
→ Does raw_value match ANY value in true_if_any list?
|
|
→ TRUE
|
|
```
|
|
|
|
#### 4d. value_labels Mapping
|
|
If `value_labels` is specified → Map value to localized text:
|
|
```json
|
|
{
|
|
"raw_value": "active",
|
|
"value_labels": [
|
|
{"value": "active", "text": {"fr": "Actif", "en": "Active"}},
|
|
{"value": "inactive", "text": {"fr": "Inactif", "en": "Inactive"}}
|
|
]
|
|
}
|
|
|
|
→ Output: "Actif" (French text)
|
|
```
|
|
|
|
#### 4e. field_template Formatting
|
|
If `field_template` is specified → Apply template with `$value` placeholder:
|
|
```
|
|
field_template: "Score: $value/100"
|
|
final_value: 85
|
|
|
|
→ Output: "Score: 85/100"
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration File Structure
|
|
|
|
### File Location
|
|
```
|
|
Endobest_Dashboard_Config.xlsx
|
|
├─ Sheet 1: "Inclusions_Mapping" (field mapping definition)
|
|
└─ Sheet 2: "Regression_Check" (non-regression rules)
|
|
[See DOCUMENTATION_12_QUALITY_CHECKS.md]
|
|
```
|
|
|
|
### Inclusions_Mapping Sheet Overview
|
|
|
|
```
|
|
Row 1 (Headers):
|
|
A B C D E
|
|
field_group field_name source_name source_id field_path
|
|
F G H I
|
|
field_template field_condition true_if_any value_labels
|
|
|
|
Row 2+: Field definitions (one per row)
|
|
```
|
|
|
|
**Color Coding** (for visual identification):
|
|
- **Yellow:** Extended fields or Calculated fields (requires special attention)
|
|
- **Blue:** Questionnaire-sourced fields (q_id, q_name, q_category)
|
|
- **Red:** Fields with errors or missing required data
|
|
- **White:** Record/Inclusion/Request fields
|
|
|
|
---
|
|
|
|
## Column Reference
|
|
|
|
### Column A: field_group
|
|
**Type:** String (required)
|
|
**Description:** Logical grouping of related fields in output JSON
|
|
**Rules:**
|
|
- Must be unique within context (same field_name can exist in different groups)
|
|
- Becomes a dictionary key in JSON: `output[field_group][field_name]`
|
|
- Controls field visibility in regression checks
|
|
|
|
**Examples:**
|
|
```
|
|
Patient_Identification → Contains patient metadata
|
|
Inclusion → Inclusion status and data
|
|
Endotest → Lab test information
|
|
Custom_Data → Default for general fields
|
|
Infos_Générales → General information
|
|
Antécédents Médicaux → Medical history
|
|
```
|
|
|
|
### Column B: field_name
|
|
**Type:** String (required)
|
|
**Description:** Unique field identifier within its group
|
|
**Rules:**
|
|
- Must not be empty
|
|
- Can contain letters, numbers, underscores, hyphens
|
|
- Special text in parentheses is automatically removed
|
|
- Example: `Patient_Age (years)` → `Patient_Age`
|
|
|
|
**Excel Behavior:** When cell contains `Patient_Age (years)`, the system parses it as:
|
|
```
|
|
field_name = "Patient_Age" # Parenthetical text stripped
|
|
```
|
|
|
|
### Column C: source_name
|
|
**Type:** String (enum)
|
|
**Required:** Yes (unless cell contains "Not Specified")
|
|
**Valid Values:**
|
|
```
|
|
Inclusion → Field from inclusion data
|
|
Record → Field from clinical record
|
|
Request → Field from lab test request
|
|
Patient / Douleurs → Questionnaire name (implicit q_name=)
|
|
Signes et symptômes → Questionnaire name (implicit q_name=)
|
|
Calculated → Custom function (no direct source)
|
|
Not Specified → Skip this row (used for spacing/comments)
|
|
```
|
|
|
|
### Column D: source_id
|
|
**Type:** String (enum with prefixes or JSON array)
|
|
**Description:** Specifies how to identify the data source
|
|
|
|
#### Format Options:
|
|
|
|
**1. Questionnaire by ID (Recommended)**
|
|
```
|
|
Syntax: q_id=<uuid>
|
|
Example: q_id=550e8400-e29b-41d4-a716-446655440000
|
|
Speed: Fastest (direct lookup)
|
|
```
|
|
|
|
**2. Questionnaire by Name**
|
|
```
|
|
Syntax: q_name=<name>
|
|
Example: q_name=Symptom Questionnaire
|
|
Speed: Slower (sequential search)
|
|
```
|
|
|
|
**3. Questionnaire by Category**
|
|
```
|
|
Syntax: q_category=<category>
|
|
Example: q_category=Medical History
|
|
Speed: Slower (sequential search)
|
|
```
|
|
|
|
**4. Record Source**
|
|
```
|
|
Value: record
|
|
Means: Extract from clinical record data
|
|
```
|
|
|
|
**5. Inclusion Source**
|
|
```
|
|
Value: inclusion
|
|
Means: Extract from inclusion metadata
|
|
```
|
|
|
|
**6. Request Source**
|
|
```
|
|
Value: request
|
|
Means: Extract from lab test request
|
|
```
|
|
|
|
**7. Calculated Function**
|
|
```
|
|
Syntax: <function_name>
|
|
Example: search_in_fields_using_regex, if_then_else, extract_parentheses_content
|
|
See Section: Custom Functions Reference
|
|
```
|
|
|
|
### Column E: field_path
|
|
**Type:** JSON array (required when field is specified)
|
|
**Description:** Path to navigate nested JSON structure
|
|
|
|
#### Syntax Examples:
|
|
|
|
**Simple field:**
|
|
```json
|
|
["name"]
|
|
// Equivalent to: data["name"]
|
|
```
|
|
|
|
**Nested path:**
|
|
```json
|
|
["record", "patient", "demographics", "age"]
|
|
// Equivalent to: data["record"]["patient"]["demographics"]["age"]
|
|
```
|
|
|
|
**Array index:**
|
|
```json
|
|
["record", "clinicResearchData", 0, "test_name"]
|
|
// Equivalent to: data["record"]["clinicResearchData"][0]["test_name"]
|
|
```
|
|
|
|
**Wildcard (all elements):**
|
|
```json
|
|
["record", "clinicResearchData", "*", "test_name"]
|
|
// Returns: [test_name_1, test_name_2, test_name_3, ...]
|
|
// Result: Automatically joined with "|" in final value
|
|
```
|
|
|
|
**For Calculated Functions (arguments):**
|
|
```json
|
|
[
|
|
"search_in_fields_using_regex",
|
|
".*surgery.*",
|
|
"Previous_Surgery",
|
|
"Recent_Surgery"
|
|
]
|
|
// First element: function name
|
|
// Rest: arguments to pass to function
|
|
```
|
|
|
|
### Column F: field_template
|
|
**Type:** String with `$value` placeholder (optional)
|
|
**Description:** Apply formatting to the final value
|
|
**Rules:**
|
|
- Only applied if final_value is not "undefined" or "N/A"
|
|
- Must contain `$value` placeholder
|
|
- Result: Template with `$value` replaced by actual value
|
|
|
|
**Examples:**
|
|
```
|
|
Template: "$value%"
|
|
Value: 85
|
|
Result: "85%"
|
|
|
|
Template: "Score: $value/100"
|
|
Value: 42
|
|
Result: "Score: 42/100"
|
|
|
|
Template: "Status: $value (Updated)"
|
|
Value: "Active"
|
|
Result: "Status: Active (Updated)"
|
|
```
|
|
|
|
### Column G: field_condition
|
|
**Type:** String (field name reference, optional)
|
|
**Description:** Conditional field inclusion based on another field's value
|
|
**Rules:**
|
|
- If specified, must reference another field name already processed
|
|
- Must evaluate to a boolean value
|
|
- Referenced as `<field_group>.<field_name>`
|
|
|
|
**Logic:**
|
|
```
|
|
IF field_condition_value == True:
|
|
Process field normally
|
|
ELIF field_condition_value == False:
|
|
Set final_value = "N/A"
|
|
ELSE (undefined/null/non-boolean):
|
|
Set final_value = "undefined"
|
|
```
|
|
|
|
**Examples:**
|
|
```
|
|
field_condition: Inclusion.isPrematurelyTerminated
|
|
Meaning: Only process this field if patient is prematurely terminated
|
|
|
|
field_condition: Endotest.Request_Sent
|
|
Meaning: Only process if test request was sent
|
|
```
|
|
|
|
### Column H: true_if_any
|
|
**Type:** JSON array (optional)
|
|
**Description:** Convert to boolean if value matches ANY item in array
|
|
|
|
**Syntax:**
|
|
```json
|
|
["value1", "value2", "value3"]
|
|
```
|
|
|
|
**Logic:**
|
|
```
|
|
LOOP through true_if_any array:
|
|
IF raw_value == any_item:
|
|
RETURN True
|
|
|
|
RETURN False
|
|
```
|
|
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_name": "Is_Active",
|
|
"true_if_any": ["active", "pending", "processing"]
|
|
}
|
|
|
|
raw_value = "pending"
|
|
→ Does "pending" exist in ["active", "pending", "processing"]?
|
|
→ YES → Final value = True
|
|
|
|
raw_value = "completed"
|
|
→ Does "completed" exist in list?
|
|
→ NO → Final value = False
|
|
```
|
|
|
|
### Column I: value_labels
|
|
**Type:** JSON array of mapping objects (optional)
|
|
**Description:** Map field values to localized text labels
|
|
|
|
**Syntax:**
|
|
```json
|
|
[
|
|
{
|
|
"value": "raw_value_1",
|
|
"text": {
|
|
"fr": "Libellé Français",
|
|
"en": "English Label"
|
|
}
|
|
},
|
|
{
|
|
"value": "raw_value_2",
|
|
"text": {
|
|
"fr": "Autre Libellé",
|
|
"en": "Another Label"
|
|
}
|
|
}
|
|
]
|
|
```
|
|
|
|
**Logic:**
|
|
```
|
|
LOOP through value_labels array:
|
|
IF label_map.value == raw_value:
|
|
RETURN label_map.text.fr (French text)
|
|
|
|
IF no match found:
|
|
RETURN "$$$$ Value Error: {raw_value}"
|
|
```
|
|
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_name": "Status",
|
|
"value_labels": [
|
|
{
|
|
"value": 1,
|
|
"text": {"fr": "Inclus", "en": "Included"}
|
|
},
|
|
{
|
|
"value": 0,
|
|
"text": {"fr": "Pré-inclus", "en": "Pre-included"}
|
|
}
|
|
]
|
|
}
|
|
|
|
raw_value = 1
|
|
→ Map to French label: "Inclus"
|
|
```
|
|
|
|
---
|
|
|
|
## Special Value Prefixes
|
|
|
|
This section documents special prefixes and keywords used in Extended Fields configuration for value resolution and field references.
|
|
|
|
### Prefix: `$` (String Literal)
|
|
|
|
**Location:** In function arguments (like `if_then_else` parameters)
|
|
|
|
**Meaning:** Marks a string value as a literal (not a field reference)
|
|
|
|
**Syntax:** `$value` (just prefix with `$`, no quotes needed)
|
|
|
|
**Without `$` prefix:**
|
|
```json
|
|
{
|
|
"field_path": ["is_true", "Has_Consent", "YES", "NO"]
|
|
}
|
|
// "YES" is interpreted as a FIELD NAME to look up
|
|
// This will fail because no field named "YES" exists
|
|
```
|
|
|
|
**With `$` prefix (correct):**
|
|
```json
|
|
{
|
|
"field_path": ["is_true", "Has_Consent", "$YES", "$NO"]
|
|
}
|
|
// $YES is interpreted as LITERAL STRING "YES"
|
|
// $NO is interpreted as LITERAL STRING "NO"
|
|
// Has_Consent is interpreted as FIELD NAME (no prefix)
|
|
```
|
|
|
|
**Why It Matters:** The system needs to distinguish between:
|
|
- **Field references** (look up values): `Status`, `Is_Active`, `Patient_Id`
|
|
- **Literal values** (use as-is): `$Active`, `$N/A`, `$Ready`
|
|
|
|
---
|
|
|
|
### No Prefix: Field References
|
|
|
|
**Location:** Arguments where field names are expected
|
|
|
|
**Meaning:** Refers to a field in the current inclusion data
|
|
|
|
**Examples:**
|
|
```json
|
|
{
|
|
"field_path": ["is_true", "Has_Consent", "$YES", "$NO"]
|
|
}
|
|
// Has_Consent ← field reference (look up this field's value)
|
|
// Status ← field reference
|
|
// Is_Active ← field reference
|
|
```
|
|
|
|
**Resolution:** The system looks up the field in the current inclusion object.
|
|
|
|
---
|
|
|
|
### Wildcard: `*` in Field Paths
|
|
|
|
**Location:** In `field_path` column (Column E in Mapping sheet)
|
|
|
|
**Meaning:** Match all elements at this level
|
|
|
|
**Syntax:**
|
|
```json
|
|
["record", "*", "results", "*", "value"]
|
|
```
|
|
|
|
**Example 1: Single Level Wildcard**
|
|
```json
|
|
{
|
|
"field_path": ["items", "*", "name"]
|
|
}
|
|
// Returns all "name" values from each item
|
|
// If items = [
|
|
// {name: "Item 1", ...},
|
|
// {name: "Item 2", ...},
|
|
// {name: "Item 3", ...}
|
|
// ]
|
|
// Result: ["Item 1", "Item 2", "Item 3"]
|
|
// Final output: "Item 1|Item 2|Item 3" (pipe-joined)
|
|
```
|
|
|
|
**Example 2: Multiple Level Wildcard**
|
|
```json
|
|
{
|
|
"field_path": ["record", "*", "data", "*", "test"]
|
|
}
|
|
// Matches test values at multiple nesting levels
|
|
```
|
|
|
|
**Post-Processing:**
|
|
- Arrays are automatically joined with `|` delimiter
|
|
- Scalar values are kept as-is
|
|
|
|
---
|
|
|
|
### Value Resolution in if_then_else
|
|
|
|
When using the `if_then_else` function, values are resolved based on their format:
|
|
|
|
| Format | Type | Resolution |
|
|
|--------|------|-----------|
|
|
| `true`, `false` | Boolean literal | Used directly |
|
|
| `42`, `3.14` | Numeric literal | Used directly |
|
|
| `$string` | String literal | Remove `$` prefix and use value |
|
|
| `field_name` | Field reference | Look up field value |
|
|
|
|
**Examples:**
|
|
```json
|
|
{
|
|
"field_path": ["is_true", "Has_Consent", "$APPROVED", "$NOT_APPROVED"]
|
|
}
|
|
// Has_Consent → field reference (look it up)
|
|
// $APPROVED → string literal (use "APPROVED")
|
|
// $NOT_APPROVED → string literal (use "NOT_APPROVED")
|
|
|
|
{
|
|
"field_path": ["==", "Status", "$Active", "Overall_Status", "$MISSING"]
|
|
}
|
|
// Status → field reference
|
|
// $Active → string literal (use "Active")
|
|
// Overall_Status → field reference
|
|
// $MISSING → string literal (use "MISSING")
|
|
```
|
|
|
|
---
|
|
|
|
## Summary Table: Special Prefixes
|
|
|
|
| Symbol | Meaning | Example |
|
|
|--------|---------|---------|
|
|
| `$value` | String literal (remove `$` prefix) | `$YES`, `$READY`, `$N/A` |
|
|
| No prefix | Field reference (look up) | `Status`, `Patient_Id` |
|
|
| `*` | Wildcard in field_path (all array elements) | `["items", "*", "name"]` |
|
|
|
|
---
|
|
|
|
## Data Sources Explained
|
|
|
|
### 1. Questionnaire Sources (q_id, q_name, q_category)
|
|
|
|
#### What Are Questionnaires?
|
|
Questionnaires are forms/surveys filled out by patients or clinicians in the Research Clinic system. Each questionnaire has:
|
|
- **ID:** Unique identifier (UUID)
|
|
- **Name:** Display name (e.g., "Symptom Assessment")
|
|
- **Category:** Logical grouping (e.g., "Medical History")
|
|
- **Answers:** Key-value pairs of responses
|
|
|
|
#### Data Structure
|
|
```json
|
|
all_questionnaires: {
|
|
"qcm-uuid-1": {
|
|
"questionnaire": {
|
|
"id": "qcm-uuid-1",
|
|
"name": "Symptom Questionnaire",
|
|
"category": "Symptoms"
|
|
},
|
|
"answers": {
|
|
"question_1": "answer_value",
|
|
"question_2": true,
|
|
"question_3": 42
|
|
}
|
|
},
|
|
"qcm-uuid-2": {
|
|
"questionnaire": {
|
|
"id": "qcm-uuid-2",
|
|
"name": "Medical History",
|
|
"category": "History"
|
|
},
|
|
"answers": {
|
|
"has_diabetes": false,
|
|
"has_hypertension": true
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Finding Questionnaires
|
|
|
|
**Option 1: By ID (Fastest)**
|
|
```json
|
|
{
|
|
"source_id": "q_id=qcm-uuid-1",
|
|
"field_path": ["answers", "question_1"]
|
|
}
|
|
// Direct lookup in dictionary by ID
|
|
// Performance: O(1) constant time
|
|
```
|
|
|
|
**Option 2: By Name**
|
|
```json
|
|
{
|
|
"source_id": "q_name=Symptom Questionnaire",
|
|
"field_path": ["answers", "question_1"]
|
|
}
|
|
// Sequential search through all questionnaires
|
|
// Performance: O(n) proportional to questionnaire count
|
|
```
|
|
|
|
**Option 3: By Category**
|
|
```json
|
|
{
|
|
"source_id": "q_category=Symptoms",
|
|
"field_path": ["answers", "question_1"]
|
|
}
|
|
// Sequential search for category match
|
|
// Performance: O(n)
|
|
```
|
|
|
|
**Recommendation:** Use `q_id=` for best performance. Name and category searches are slower but acceptable if IDs are not available.
|
|
|
|
### 2. Record Source (Clinical Data)
|
|
|
|
#### What Is Record Data?
|
|
The clinical record contains all medical information for a patient within the Research Clinic context:
|
|
- Protocol inclusions status
|
|
- Clinical research data (test requests, results)
|
|
- Patient demographics
|
|
- Medical history
|
|
|
|
#### Data Structure
|
|
```json
|
|
record_data: {
|
|
"record": {
|
|
"id": "record-uuid",
|
|
"patientId": "patient-uuid",
|
|
"protocol_inclusions": [
|
|
{
|
|
"status": "incluse",
|
|
"blockedQcmVersions": [],
|
|
"clinicResearchData": [
|
|
{
|
|
"requestMetaData": {
|
|
"tubeId": "tube-uuid-123"
|
|
},
|
|
"needRcp": false
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Example Extraction
|
|
```json
|
|
{
|
|
"source_id": "record",
|
|
"field_path": ["record", "protocol_inclusions", 0, "status"]
|
|
}
|
|
// Result: "incluse"
|
|
|
|
{
|
|
"source_id": "record",
|
|
"field_path": ["record", "clinicResearchData", "*", "requestMetaData", "tubeId"]
|
|
}
|
|
// Result: ["tube-uuid-1", "tube-uuid-2"]
|
|
// Final: "tube-uuid-1|tube-uuid-2"
|
|
```
|
|
|
|
### 3. Inclusion Source (Inclusion Metadata)
|
|
|
|
#### What Is Inclusion Data?
|
|
Inclusion data contains metadata about the patient's inclusion in the research protocol:
|
|
- Basic patient information (name, birthday)
|
|
- Organization assignment
|
|
- Inclusion status
|
|
- Inclusion date
|
|
|
|
#### Data Structure
|
|
```json
|
|
inclusion_data: {
|
|
"id": "patient-uuid",
|
|
"name": "Doe, John",
|
|
"birthday": "1975-05-15",
|
|
"status": "incluse",
|
|
"inclusionDate": "2024-10-15",
|
|
"organization_id": "org-uuid-added-by-system",
|
|
"organization_name": "Center Name-added-by-system"
|
|
}
|
|
```
|
|
|
|
#### Example Extraction
|
|
```json
|
|
{
|
|
"source_id": "inclusion",
|
|
"field_path": ["name"]
|
|
}
|
|
// Result: "Doe, John"
|
|
|
|
{
|
|
"source_id": "inclusion",
|
|
"field_path": ["status"]
|
|
}
|
|
// Result: "incluse"
|
|
```
|
|
|
|
### 4. Request Source (Lab Test Data)
|
|
|
|
#### What Is Request Data?
|
|
Request data contains information about laboratory tests ordered and their results:
|
|
- Test request status
|
|
- Diagnostic status
|
|
- Individual test results
|
|
- Result values
|
|
|
|
#### Data Structure
|
|
```json
|
|
request_data: {
|
|
"id": "request-uuid",
|
|
"tubeId": "tube-uuid-123",
|
|
"status": "completed",
|
|
"diagnostic_status": "Completed",
|
|
"results": [
|
|
{
|
|
"testName": "Complete Blood Count",
|
|
"value": "Normal",
|
|
"unit": ""
|
|
},
|
|
{
|
|
"testName": "Coelioscopie",
|
|
"value": "Findings documented",
|
|
"unit": ""
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Example Extraction
|
|
```json
|
|
{
|
|
"source_id": "request",
|
|
"field_path": ["status"]
|
|
}
|
|
// Result: "completed"
|
|
|
|
{
|
|
"source_id": "request",
|
|
"field_path": ["results", "*", "testName"]
|
|
}
|
|
// Result: ["Complete Blood Count", "Coelioscopie"]
|
|
// Final: "Complete Blood Count|Coelioscopie"
|
|
```
|
|
|
|
### 5. Calculated Source (Custom Functions)
|
|
|
|
#### What Are Calculated Fields?
|
|
Calculated fields derive their values from custom business logic functions, not direct data extraction. The function can access other already-processed fields and perform complex transformations.
|
|
|
|
#### Examples
|
|
```json
|
|
{
|
|
"source_name": "Calculated",
|
|
"source_id": "search_in_fields_using_regex",
|
|
"field_path": [".*SURGERY.*", "Previous_Surgery", "Recent_Surgery"]
|
|
}
|
|
// Function searches multiple fields using regex
|
|
|
|
{
|
|
"source_name": "Calculated",
|
|
"source_id": "if_then_else",
|
|
"field_path": ["is_true", "Requested", "$\"YES\"", "$\"NO\""]
|
|
}
|
|
// Function applies conditional logic
|
|
|
|
{
|
|
"source_name": "Calculated",
|
|
"source_id": "extract_parentheses_content",
|
|
"field_path": ["Status_Field"]
|
|
}
|
|
// Function extracts text from within parentheses
|
|
```
|
|
|
|
See **Section: Custom Functions Reference** for detailed function documentation.
|
|
|
|
### 6. Inclusion Source with Organization Enrichment (center_name)
|
|
|
|
#### What Is Organization Center Mapping?
|
|
|
|
The organization center mapping feature enriches patient inclusion data with standardized center identifiers. When configured, the `center_name` field is automatically added to each inclusion record, allowing you to group patients by center codes.
|
|
|
|
#### Data Source: Inclusion Type
|
|
|
|
```json
|
|
{
|
|
"source_name": "Inclusion",
|
|
"source_id": "inclusion",
|
|
"source_type": "inclusion",
|
|
"field_path": ["center_name"]
|
|
}
|
|
```
|
|
|
|
#### Fields Available from Organization Enrichment
|
|
|
|
| Field | Type | Description | Availability |
|
|
|-------|------|-------------|--------------|
|
|
| `center_name` | String | Standardized center identifier | If mapping file exists |
|
|
| `organization_name` | String | Full organization name | Always |
|
|
| `organization_id` | String | Organization UUID | Always |
|
|
|
|
#### Data Structure
|
|
|
|
```json
|
|
inclusion_data: {
|
|
"organization_id": "org-uuid",
|
|
"organization_name": "Hospital Cardiology Research Lab",
|
|
"center_name": "HCR-MAIN", // ← Added by organization mapping
|
|
"id": "patient-uuid",
|
|
...
|
|
}
|
|
```
|
|
|
|
#### Example Extraction
|
|
|
|
```json
|
|
{
|
|
"source_name": "Inclusion",
|
|
"source_id": "inclusion",
|
|
"source_type": "inclusion",
|
|
"field_path": ["center_name"]
|
|
}
|
|
// Result: "HCR-MAIN"
|
|
|
|
{
|
|
"source_name": "Inclusion",
|
|
"source_id": "inclusion",
|
|
"source_type": "inclusion",
|
|
"field_path": ["organization_name"]
|
|
}
|
|
// Result: "Hospital Cardiology Research Lab"
|
|
```
|
|
|
|
#### Configuration Requirements
|
|
|
|
**To use this feature:**
|
|
|
|
1. Create `eb_org_center_mapping.xlsx` in script directory (see [DOCUMENTATION_10_ARCHITECTURE.md](DOCUMENTATION_10_ARCHITECTURE.md) Organization ↔ Center Mapping section)
|
|
2. Define mapping rules in the `Org_Center_Mapping` sheet
|
|
3. Add extended field with source type "inclusion" and field_path ["center_name"]
|
|
|
|
**Availability:**
|
|
- ✅ If mapping file exists and organization is mapped → `center_name` = mapped value
|
|
- ⚠️ If mapping file missing or organization not in mapping → `center_name` = organization name (fallback)
|
|
|
|
#### Example Configuration
|
|
|
|
```json
|
|
{
|
|
"field_group": "Patient_Identification",
|
|
"field_name": "Center_Name",
|
|
"source_name": "Inclusion",
|
|
"source_id": "inclusion",
|
|
"source_type": "inclusion",
|
|
"field_path": ["center_name"],
|
|
"field_template": null,
|
|
"field_condition": null,
|
|
"true_if_any": null,
|
|
"value_labels": null
|
|
}
|
|
```
|
|
|
|
**Result in output:**
|
|
```json
|
|
{
|
|
"Patient_Identification": {
|
|
"Organisation_Name": "Hospital Cardiology Research Lab",
|
|
"Center_Name": "HCR-MAIN",
|
|
...
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Field Path Syntax
|
|
|
|
### Basic Path Navigation
|
|
|
|
#### Single-Level Access
|
|
```json
|
|
["field_name"]
|
|
// JavaScript equivalent: data.field_name
|
|
// Result: value or undefined
|
|
```
|
|
|
|
#### Multi-Level Nesting
|
|
```json
|
|
["record", "patient", "demographics", "age"]
|
|
// JavaScript: data.record.patient.demographics.age
|
|
```
|
|
|
|
#### Array Index Access
|
|
```json
|
|
["items", 0, "name"]
|
|
// JavaScript: data.items[0].name
|
|
// Accesses first element of array
|
|
```
|
|
|
|
#### Negative Index (from end)
|
|
```json
|
|
["items", -1, "name"]
|
|
// JavaScript: data.items[data.items.length - 1].name
|
|
// Accesses last element of array
|
|
```
|
|
|
|
### Wildcard Paths (Multiple Values)
|
|
|
|
#### Single Wildcard (One Level)
|
|
```json
|
|
["questionnaire", "answers", "*", "value"]
|
|
// Returns all values from each answer object
|
|
// Result: Array of values [value1, value2, value3, ...]
|
|
```
|
|
|
|
#### Multiple Wildcards (Deep)
|
|
```json
|
|
["record", "*", "data", "*", "test"]
|
|
// Matches nested wildcards at multiple levels
|
|
// Returns: All tests at matching paths
|
|
```
|
|
|
|
#### Wildcard Result Flattening
|
|
```json
|
|
path: ["items", "*", "values", "*", "score"]
|
|
items: [
|
|
{
|
|
"values": [
|
|
{"score": 10},
|
|
{"score": 20}
|
|
]
|
|
},
|
|
{
|
|
"values": [
|
|
{"score": 30},
|
|
{"score": 40}
|
|
]
|
|
}
|
|
]
|
|
|
|
// Without flattening: [[10, 20], [30, 40]]
|
|
// With flattening (used): [10, 20, 30, 40]
|
|
```
|
|
|
|
### Edge Cases & Behavior
|
|
|
|
#### Missing Path
|
|
```json
|
|
field_path: ["missing", "field"]
|
|
data: {}
|
|
|
|
Result: "undefined" (not null or empty string)
|
|
```
|
|
|
|
#### Null/None Values in Path
|
|
```json
|
|
field_path: ["patient", "contact", "phone"]
|
|
data: {"patient": {"contact": null}}
|
|
|
|
Result: "undefined" (stops at null)
|
|
```
|
|
|
|
#### Non-Dictionary/Non-List Element
|
|
```json
|
|
field_path: ["patient", "name", "first"]
|
|
data: {"patient": {"name": "John"}} // "name" is string, not dict
|
|
|
|
Result: "undefined" (cannot navigate string)
|
|
```
|
|
|
|
---
|
|
|
|
## Custom Functions Reference
|
|
|
|
### Function 1: search_in_fields_using_regex
|
|
|
|
**Purpose:** Search multiple fields for regex pattern match (case-insensitive)
|
|
|
|
**Syntax:**
|
|
```json
|
|
{
|
|
"source_name": "Calculated",
|
|
"source_id": "search_in_fields_using_regex",
|
|
"field_path": ["regex_pattern", "field_1", "field_2", ...]
|
|
}
|
|
```
|
|
|
|
**Parameters:**
|
|
- **regex_pattern** (string): Regular expression pattern (case-insensitive)
|
|
- **field_1, field_2, ...** (strings): Field names to search (looked up in output_inclusion)
|
|
|
|
**Logic:**
|
|
```
|
|
FOR EACH field in [field_1, field_2, ...]:
|
|
value = get_value_from_inclusion(field_name)
|
|
IF value is string AND value matches regex_pattern:
|
|
RETURN True
|
|
|
|
RETURN False
|
|
```
|
|
|
|
**Return Value:**
|
|
- `True` if ANY field matches the pattern
|
|
- `False` if NO fields match
|
|
- `"undefined"` if ALL fields are undefined
|
|
|
|
**Examples:**
|
|
|
|
Example 1: Detect if any surgery field contains "surgery"
|
|
```json
|
|
{
|
|
"field_name": "Has_Surgery_History",
|
|
"source_id": "search_in_fields_using_regex",
|
|
"field_path": [".*surgery.*", "Previous_Surgery", "Recent_Surgery", "Planned_Surgery"]
|
|
}
|
|
|
|
If any of these fields contains "surgery" → True
|
|
Otherwise → False
|
|
```
|
|
|
|
Example 2: Check for specific procedures
|
|
```json
|
|
{
|
|
"field_name": "Is_Endoscopy_Planned",
|
|
"source_id": "search_in_fields_using_regex",
|
|
"field_path": ["endoscopy|colonoscopy", "Procedure_Type", "Procedure_Notes"]
|
|
}
|
|
|
|
Matches if "endoscopy" OR "colonoscopy" appears in either field
|
|
```
|
|
|
|
### Function 2: extract_parentheses_content
|
|
|
|
**Purpose:** Extract text within the first set of parentheses
|
|
|
|
**Syntax:**
|
|
```json
|
|
{
|
|
"source_name": "Calculated",
|
|
"source_id": "extract_parentheses_content",
|
|
"field_path": ["field_name"]
|
|
}
|
|
```
|
|
|
|
**Parameters:**
|
|
- **field_name** (string): Field to extract from (looked up in output_inclusion)
|
|
|
|
**Logic:**
|
|
```
|
|
value = get_value_from_inclusion(field_name)
|
|
IF value is not defined:
|
|
RETURN "undefined"
|
|
|
|
MATCH first occurrence of (content) pattern
|
|
IF match found:
|
|
RETURN content
|
|
ELSE:
|
|
RETURN "undefined"
|
|
```
|
|
|
|
**Return Value:**
|
|
- Text extracted from parentheses (e.g., "Active")
|
|
- `"undefined"` if no parentheses found or field undefined
|
|
|
|
**Examples:**
|
|
|
|
Example 1: Extract status from formatted field
|
|
```
|
|
Input: "Patient Status (Active)"
|
|
Output: "Active"
|
|
```
|
|
|
|
Example 2: Extract category name
|
|
```
|
|
Input: "Medical Condition (Hypertension)"
|
|
Output: "Hypertension"
|
|
```
|
|
|
|
Example 3: Nested extraction
|
|
```
|
|
Input: "Surgery Scheduled (Appendectomy - Jan 15)"
|
|
Output: "Appendectomy - Jan 15"
|
|
```
|
|
|
|
### Function 3: append_terminated_suffix
|
|
|
|
**Purpose:** Add " - AP" suffix to status if patient prematurely terminated
|
|
|
|
**Syntax:**
|
|
```json
|
|
{
|
|
"source_name": "Calculated",
|
|
"source_id": "append_terminated_suffix",
|
|
"field_path": ["status_field_name", "is_terminated_field_name"]
|
|
}
|
|
```
|
|
|
|
**Parameters:**
|
|
- **status_field_name** (string): Field containing status value
|
|
- **is_terminated_field_name** (string): Boolean field indicating termination
|
|
|
|
**Logic:**
|
|
```
|
|
status = get_value_from_inclusion(status_field_name)
|
|
is_terminated = get_value_from_inclusion(is_terminated_field_name)
|
|
|
|
IF status is undefined:
|
|
RETURN "undefined"
|
|
|
|
IF is_terminated is TRUE:
|
|
RETURN status + " - AP"
|
|
ELSE:
|
|
RETURN status
|
|
```
|
|
|
|
**Return Value:**
|
|
- Status with " - AP" suffix if terminated
|
|
- Original status if not terminated
|
|
- `"undefined"` if status field undefined
|
|
|
|
**Examples:**
|
|
|
|
Example 1: Mark prematurely terminated patients
|
|
```json
|
|
{
|
|
"field_name": "Inclusion_Status",
|
|
"source_id": "append_terminated_suffix",
|
|
"field_path": ["Base_Status", "isPrematurelyTerminated"]
|
|
}
|
|
|
|
If isPrematurelyTerminated = True:
|
|
"incluse" → "incluse - AP"
|
|
|
|
If isPrematurelyTerminated = False:
|
|
"incluse" → "incluse"
|
|
```
|
|
|
|
### Function 4: if_then_else
|
|
|
|
**Purpose:** Unified conditional logic with 8 different operators
|
|
|
|
**Syntax:**
|
|
```json
|
|
{
|
|
"source_name": "Calculated",
|
|
"source_id": "if_then_else",
|
|
"field_path": ["operator", arg1, arg2_optional, result_if_true, result_if_false]
|
|
}
|
|
```
|
|
|
|
#### Operator Reference
|
|
|
|
##### Operator 1: is_true
|
|
**Signature:** `["is_true", field_name, result_if_true, result_if_false]`
|
|
**Logic:** IF field == True THEN result_if_true ELSE result_if_false
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_path": ["is_true", "Has_Consent", "$\"Consented\"", "$\"Not Consented\""]
|
|
}
|
|
// If Has_Consent = True → "Consented"
|
|
// If Has_Consent = False → "Not Consented"
|
|
```
|
|
|
|
##### Operator 2: is_false
|
|
**Signature:** `["is_false", field_name, result_if_true, result_if_false]`
|
|
**Logic:** IF field == False THEN result_if_true ELSE result_if_false
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_path": ["is_false", "Has_Exclusion", "$\"Eligible\"", "$\"Excluded\""]
|
|
}
|
|
```
|
|
|
|
##### Operator 3: is_defined
|
|
**Signature:** `["is_defined", field_name, result_if_true, result_if_false]`
|
|
**Logic:** IF field is not undefined THEN result_if_true ELSE result_if_false
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_path": ["is_defined", "Surgery_Date", "$\"Date Available\"", "$\"No Date\""]
|
|
}
|
|
```
|
|
|
|
##### Operator 4: is_undefined
|
|
**Signature:** `["is_undefined", field_name, result_if_true, result_if_false]`
|
|
**Logic:** IF field is undefined THEN result_if_true ELSE result_if_false
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_path": ["is_undefined", "Last_Contact", "$\"Never Contacted\"", "$\"Contacted\""]
|
|
}
|
|
```
|
|
|
|
##### Operator 5: all_true
|
|
**Signature:** `["all_true", [field_1, field_2, ...], result_if_true, result_if_false]`
|
|
**Logic:** IF all fields == True THEN result_if_true ELSE result_if_false
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_path": ["all_true", ["Has_Consent", "Has_Results", "Is_Complete"], "$\"READY\"", "$\"INCOMPLETE\""]
|
|
}
|
|
// Returns "READY" only if ALL three fields are True
|
|
```
|
|
|
|
##### Operator 6: all_defined
|
|
**Signature:** `["all_defined", [field_1, field_2, ...], result_if_true, result_if_false]`
|
|
**Logic:** IF all fields are defined THEN result_if_true ELSE result_if_false
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_path": ["all_defined", ["First_Name", "Last_Name", "Birth_Date"], "$\"COMPLETE\"", "$\"INCOMPLETE\""]
|
|
}
|
|
// Returns "COMPLETE" only if ALL three fields have values
|
|
```
|
|
|
|
##### Operator 7: ==
|
|
**Signature:** `["==", value1, value2, result_if_true, result_if_false]`
|
|
**Logic:** IF value1 == value2 THEN result_if_true ELSE result_if_false
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_path": ["==", "Status", "$\"Active\"", "$\"Is Active\"", "$\"Not Active\""]
|
|
}
|
|
// If Status equals "Active" → "Is Active"
|
|
```
|
|
|
|
##### Operator 8: !=
|
|
**Signature:** `["!=", value1, value2, result_if_true, result_if_false]`
|
|
**Logic:** IF value1 != value2 THEN result_if_true ELSE result_if_false
|
|
**Example:**
|
|
```json
|
|
{
|
|
"field_path": ["!=", "Status", "$\"Inactive\"", "$\"Active\"", "$\"Inactive\""]
|
|
}
|
|
// If Status NOT equal to "Inactive" → "Active"
|
|
```
|
|
|
|
#### Value Resolution
|
|
|
|
The function supports multiple value types:
|
|
|
|
**Boolean Literals:**
|
|
```json
|
|
true, false
|
|
// Used directly without field lookup
|
|
```
|
|
|
|
**Numeric Literals:**
|
|
```json
|
|
42, 3.14, 0, -1
|
|
// Used directly without field lookup
|
|
```
|
|
|
|
**String Literals (Prefixed with $):**
|
|
```json
|
|
"$\"Active\"", "$\"Ready\"", "$\"N/A\""
|
|
// Remove $ prefix before using
|
|
// $ prefix signals: don't look this up as field name
|
|
```
|
|
|
|
**Field References (No Prefix):**
|
|
```json
|
|
"Status", "Is_Active", "Patient_Name"
|
|
// Looked up in output_inclusion
|
|
```
|
|
|
|
**Complex Examples:**
|
|
```json
|
|
{
|
|
"field_path": ["==", "Status_Code", 1, "$\"Active\"", "$\"Inactive\""]
|
|
}
|
|
// Compare Status_Code field against numeric value 1
|
|
|
|
{
|
|
"field_path": ["all_true", ["Consent_Received", "Test_Completed"], "Overall_Status", "$\"MISSING\""]
|
|
}
|
|
// If both conditions true, use Overall_Status value
|
|
// If either false, use literal "MISSING"
|
|
```
|
|
|
|
---
|
|
|
|
## Post-Processing Transformations
|
|
|
|
### Transformation Order
|
|
|
|
```
|
|
Raw Value Extraction
|
|
↓
|
|
Condition Check
|
|
↓
|
|
IF final_value is list:
|
|
└─ Join with "|" delimiter
|
|
↓
|
|
IF final_value is score dict (has 'total' and 'max'):
|
|
└─ Format as "total/max"
|
|
↓
|
|
IF true_if_any is specified:
|
|
└─ Apply boolean conversion
|
|
↓
|
|
IF value_labels is specified:
|
|
└─ Apply label mapping
|
|
↓
|
|
IF field_template is specified:
|
|
└─ Apply formatting with $value
|
|
```
|
|
|
|
### Transformation 1: Array Flattening
|
|
|
|
**When:** Raw value is an array/list
|
|
**Action:** Join elements with `|` delimiter
|
|
**Example:**
|
|
```
|
|
Raw: ["Active", "Pending", "Resolved"]
|
|
Output: "Active|Pending|Resolved"
|
|
```
|
|
|
|
### Transformation 2: Score Dictionary Formatting
|
|
|
|
**When:** Raw value is dict with keys ['total', 'max']
|
|
**Action:** Convert to "total/max" string format
|
|
**Example:**
|
|
```
|
|
Raw: {"total": 8, "max": 10}
|
|
Output: "8/10"
|
|
```
|
|
|
|
### Transformation 3: true_if_any
|
|
|
|
**When:** true_if_any is specified in configuration
|
|
**Action:** Check if raw value matches ANY item in the array
|
|
**Example:**
|
|
```json
|
|
{
|
|
"true_if_any": ["Active", "Pending", "Processing"],
|
|
"raw_value": "Active"
|
|
}
|
|
// Result: true
|
|
|
|
{
|
|
"true_if_any": ["Active", "Pending"],
|
|
"raw_value": "Completed"
|
|
}
|
|
// Result: false
|
|
```
|
|
|
|
### Transformation 4: value_labels
|
|
|
|
**When:** value_labels is specified in configuration
|
|
**Action:** Map raw value to localized text
|
|
**Logic:**
|
|
```
|
|
FOR EACH label_map in value_labels:
|
|
IF label_map.value == raw_value:
|
|
RETURN label_map.text.fr (French label)
|
|
|
|
IF no match:
|
|
RETURN "$$$$ Value Error: {raw_value}"
|
|
```
|
|
|
|
**Example:**
|
|
```json
|
|
{
|
|
"value_labels": [
|
|
{"value": "active", "text": {"fr": "Actif", "en": "Active"}},
|
|
{"value": "inactive", "text": {"fr": "Inactif", "en": "Inactive"}}
|
|
],
|
|
"raw_value": "active"
|
|
}
|
|
// Result: "Actif"
|
|
```
|
|
|
|
### Transformation 5: field_template
|
|
|
|
**When:** field_template is specified (and value is not "undefined" or "N/A")
|
|
**Action:** Replace $value placeholder with actual value
|
|
**Example:**
|
|
```
|
|
template: "Score: $value/100"
|
|
raw_value: 85
|
|
Result: "Score: 85/100"
|
|
|
|
template: "Status [$value]"
|
|
raw_value: "Active"
|
|
Result: "Status [Active]"
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration Examples
|
|
|
|
### Example 1: Simple Field Extraction
|
|
|
|
**Requirement:** Extract patient name from inclusion data
|
|
|
|
```json
|
|
{
|
|
"field_group": "Patient_Identification",
|
|
"field_name": "Patient_Name",
|
|
"source_name": "Inclusion",
|
|
"source_id": "inclusion",
|
|
"field_path": ["name"],
|
|
"field_template": null,
|
|
"field_condition": null,
|
|
"true_if_any": null,
|
|
"value_labels": null
|
|
}
|
|
```
|
|
|
|
**Flow:**
|
|
1. Source: inclusion data
|
|
2. Extract: data["name"]
|
|
3. Result: "Doe, John"
|
|
4. Output: {"Patient_Identification": {"Patient_Name": "Doe, John"}}
|
|
|
|
### Example 2: Questionnaire Field with Label Mapping
|
|
|
|
**Requirement:** Extract symptom severity and map to French labels
|
|
|
|
```json
|
|
{
|
|
"field_group": "Symptoms",
|
|
"field_name": "Severity",
|
|
"source_name": "Symptoms (OUI/NON)",
|
|
"source_id": "q_id=77e488a1-d3c-148af-a6bc-8fe1f55e82e4",
|
|
"field_path": ["answers", "question5"],
|
|
"field_template": null,
|
|
"field_condition": null,
|
|
"true_if_any": null,
|
|
"value_labels": [
|
|
{"value": 1, "text": {"fr": "Léger", "en": "Mild"}},
|
|
{"value": 2, "text": {"fr": "Modéré", "en": "Moderate"}},
|
|
{"value": 3, "text": {"fr": "Sévère", "en": "Severe"}}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Flow:**
|
|
1. Source: Questionnaire with ID 77e488a1-...
|
|
2. Extract: answers["question5"] → 2
|
|
3. Apply value_labels: 2 → "Modéré"
|
|
4. Output: {"Symptoms": {"Severity": "Modéré"}}
|
|
|
|
### Example 3: Conditional Field
|
|
|
|
**Requirement:** Only show request status if test was requested
|
|
|
|
```json
|
|
{
|
|
"field_group": "Endotest",
|
|
"field_name": "Request_Status",
|
|
"source_name": "Request",
|
|
"source_id": "request",
|
|
"field_path": ["status"],
|
|
"field_template": null,
|
|
"field_condition": "Endotest.Request_Sent",
|
|
"true_if_any": null,
|
|
"value_labels": null
|
|
}
|
|
```
|
|
|
|
**Flow:**
|
|
1. Check condition: Endotest.Request_Sent
|
|
2. If False → Set to "N/A"
|
|
3. If True → Extract status from request data
|
|
4. Output: {"Endotest": {"Request_Status": "completed"}} or "N/A"
|
|
|
|
### Example 4: Calculated Field with if_then_else
|
|
|
|
**Requirement:** Show overall status based on inclusion and termination
|
|
|
|
```json
|
|
{
|
|
"field_group": "Inclusion",
|
|
"field_name": "Inclusion_Status_Complete",
|
|
"source_name": "Calculated",
|
|
"source_id": "if_then_else",
|
|
"field_path": ["is_true", "isPrematurelyTerminated", "$\"incluse - AP\"", "Inclusion_Status"],
|
|
"field_template": null,
|
|
"field_condition": null,
|
|
"true_if_any": null,
|
|
"value_labels": null
|
|
}
|
|
```
|
|
|
|
**Flow:**
|
|
1. Check: Is isPrematurelyTerminated == True?
|
|
2. If YES → Return literal "incluse - AP"
|
|
3. If NO → Return value of Inclusion_Status field
|
|
4. Output: {"Inclusion": {"Inclusion_Status_Complete": "incluse - AP"}} or "incluse"
|
|
|
|
### Example 5: Array Field with Formatting
|
|
|
|
**Requirement:** Extract all test names and format them
|
|
|
|
```json
|
|
{
|
|
"field_group": "Endotest",
|
|
"field_name": "Tests_Performed",
|
|
"source_name": "Request",
|
|
"source_id": "request",
|
|
"field_path": ["results", "*", "testName"],
|
|
"field_template": "Tests: $value",
|
|
"field_condition": null,
|
|
"true_if_any": null,
|
|
"value_labels": null
|
|
}
|
|
```
|
|
|
|
**Flow:**
|
|
1. Source: request data
|
|
2. Extract: results[*].testName → ["Blood Test", "Imaging", "ECG"]
|
|
3. Array flattening → "Blood Test|Imaging|ECG"
|
|
4. Apply template → "Tests: Blood Test|Imaging|ECG"
|
|
5. Output: {"Endotest": {"Tests_Performed": "Tests: Blood Test|Imaging|ECG"}}
|
|
|
|
### Example 6: Complex Conditional Logic
|
|
|
|
**Requirement:** Show surgery type based on multiple conditions
|
|
|
|
```json
|
|
{
|
|
"field_group": "Surgery",
|
|
"field_name": "Surgery_Status",
|
|
"source_name": "Calculated",
|
|
"source_id": "if_then_else",
|
|
"field_path": [
|
|
"all_true",
|
|
["Surgery_Planned", "Surgeon_Assigned", "Date_Set"],
|
|
"$\"READY_FOR_SURGERY\"",
|
|
"$\"INCOMPLETE_PREPARATION\""
|
|
],
|
|
"field_template": null,
|
|
"field_condition": null,
|
|
"true_if_any": null,
|
|
"value_labels": null
|
|
}
|
|
```
|
|
|
|
**Flow:**
|
|
1. Check: Are ALL of [Surgery_Planned, Surgeon_Assigned, Date_Set] == True?
|
|
2. If YES → "READY_FOR_SURGERY"
|
|
3. If NO → "INCOMPLETE_PREPARATION"
|
|
4. Output: Conditional status
|
|
|
|
### Example 7: Search and Boolean Conversion
|
|
|
|
**Requirement:** Detect if patient has surgery history
|
|
|
|
```json
|
|
{
|
|
"field_group": "Medical_History",
|
|
"field_name": "Has_Prior_Surgery",
|
|
"source_name": "Calculated",
|
|
"source_id": "search_in_fields_using_regex",
|
|
"field_path": [".*surgery|.*intervention.*", "History_Notes", "Previous_Procedures"],
|
|
"field_template": null,
|
|
"field_condition": null,
|
|
"true_if_any": null,
|
|
"value_labels": null
|
|
}
|
|
```
|
|
|
|
**Flow:**
|
|
1. Search History_Notes and Previous_Procedures
|
|
2. Pattern: ".*surgery|.*intervention.*" (case-insensitive)
|
|
3. If ANY field matches → true
|
|
4. If NO matches → false
|
|
5. Output: {"Medical_History": {"Has_Prior_Surgery": true}}
|
|
|
|
---
|
|
|
|
## User Guide: Adding/Modifying Fields
|
|
|
|
### Step 1: Identify Data Source
|
|
|
|
Determine where the data lives:
|
|
```
|
|
Patient Name → inclusion (inclusion_data)
|
|
Symptom Severity → questionnaire (q_id, q_name, or q_category)
|
|
Clinical Notes → record (record_data)
|
|
Test Results → request (request_data)
|
|
Derived Value → calculated (custom function)
|
|
```
|
|
|
|
### Step 2: Locate Field Path
|
|
|
|
Navigate the JSON structure to find the exact path:
|
|
|
|
**For Inclusion:**
|
|
```
|
|
Open endobest_inclusions_old.json
|
|
Find a patient record
|
|
Look for field under "Patient_Identification"
|
|
Example path: ["name"]
|
|
```
|
|
|
|
**For Questionnaire:**
|
|
```
|
|
Need questionnaire ID/name/category
|
|
Look inside answers object
|
|
Example: q_id=abc-123, field_path: ["answers", "question_5"]
|
|
```
|
|
|
|
**For Record:**
|
|
```
|
|
Open a record with GET /api/records/byPatient
|
|
Navigate structure
|
|
Example: ["record", "clinicResearchData", 0, "requestMetaData"]
|
|
```
|
|
|
|
**For Request:**
|
|
```
|
|
Field from lab request response
|
|
Example: ["results", "*", "testName"]
|
|
```
|
|
|
|
### Step 3: Create Configuration Row
|
|
|
|
Open Endobest_Dashboard_Config.xlsx → Inclusions_Mapping sheet
|
|
|
|
```
|
|
Row N:
|
|
A: field_group (e.g., "Custom_Data")
|
|
B: field_name (e.g., "Patient_Status")
|
|
C: source_name (e.g., "Inclusion")
|
|
D: source_id (e.g., "inclusion")
|
|
E: field_path (e.g., ["status"])
|
|
F: field_template (optional, e.g., "Status: $value")
|
|
G: field_condition (optional, e.g., "Inclusion.Is_Active")
|
|
H: true_if_any (optional, e.g., ["active", "pending"])
|
|
I: value_labels (optional, complex JSON)
|
|
```
|
|
|
|
### Step 4: Validate Configuration
|
|
|
|
Run the dashboard in check-only mode:
|
|
```bash
|
|
python eb_dashboard.py --check-only
|
|
```
|
|
|
|
**Expected Output:**
|
|
```
|
|
✓ Loaded 81 fields from extended configuration.
|
|
✓ All checks passed successfully!
|
|
```
|
|
|
|
**If errors occur:**
|
|
```
|
|
Error in config file, row 42, field 'field_path': Invalid JSON format.
|
|
```
|
|
→ Fix the JSON syntax in the cell
|
|
|
|
### Step 5: Test with Full Collection
|
|
|
|
```bash
|
|
python eb_dashboard.py
|
|
```
|
|
|
|
After collection completes, verify:
|
|
1. New field appears in endobest_inclusions.json
|
|
2. Values are populated correctly
|
|
3. No data quality issues reported
|
|
|
|
### Step 6: Document the Field
|
|
|
|
Add comments in a separate notes section (if available) explaining:
|
|
- Purpose of the field
|
|
- Data source and ID
|
|
- Any special transformations
|
|
- Expected value ranges/types
|
|
|
|
---
|
|
|
|
## Common Patterns & Recipes
|
|
|
|
### Pattern 1: Boolean Flag from Multiple Conditions
|
|
|
|
**Requirement:** Create true/false flag based on multiple fields
|
|
|
|
```json
|
|
{
|
|
"field_group": "Flags",
|
|
"field_name": "Is_Ready_For_Export",
|
|
"source_name": "Calculated",
|
|
"source_id": "if_then_else",
|
|
"field_path": [
|
|
"all_true",
|
|
["Has_Consent", "Data_Complete", "Approved"],
|
|
true,
|
|
false
|
|
]
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Score Display Formatting
|
|
|
|
**Requirement:** Show quality of life score as "X/100" format
|
|
|
|
```json
|
|
{
|
|
"field_group": "Quality_Metrics",
|
|
"field_name": "QOL_Score_Display",
|
|
"source_name": "q_id=...",
|
|
"source_id": "q_id=...",
|
|
"field_path": ["answers", "overall_score"],
|
|
"field_template": "$value/100"
|
|
}
|
|
```
|
|
|
|
### Pattern 3: Status Translation with Suffix
|
|
|
|
**Requirement:** Show inclusion status with " - AP" for terminated patients
|
|
|
|
```json
|
|
{
|
|
"field_group": "Inclusion",
|
|
"field_name": "Status_With_Termination",
|
|
"source_name": "Calculated",
|
|
"source_id": "append_terminated_suffix",
|
|
"field_path": ["Inclusion_Status", "isPrematurelyTerminated"]
|
|
}
|
|
```
|
|
|
|
### Pattern 4: List-to-String Conversion
|
|
|
|
**Requirement:** Show all diagnoses as pipe-separated text
|
|
|
|
```json
|
|
{
|
|
"field_group": "Medical_Data",
|
|
"field_name": "All_Diagnoses",
|
|
"source_name": "Record",
|
|
"source_id": "record",
|
|
"field_path": ["record", "diagnoses", "*", "code"]
|
|
// Result: "ICD-001|ICD-002|ICD-003"
|
|
}
|
|
```
|
|
|
|
### Pattern 5: Optional Field Based on Condition
|
|
|
|
**Requirement:** Only show surgery details if surgery was performed
|
|
|
|
```json
|
|
{
|
|
"field_group": "Surgery",
|
|
"field_name": "Surgery_Details",
|
|
"source_name": "Record",
|
|
"source_id": "record",
|
|
"field_path": ["record", "surgery", "details"],
|
|
"field_condition": "Surgery.Surgery_Performed"
|
|
// If Surgery_Performed = false → "N/A"
|
|
}
|
|
```
|
|
|
|
### Pattern 6: Enum-to-Text Mapping
|
|
|
|
**Requirement:** Convert numeric status codes to readable text
|
|
|
|
```json
|
|
{
|
|
"field_group": "Status",
|
|
"field_name": "Inclusion_Status_Text",
|
|
"source_name": "Inclusion",
|
|
"source_id": "inclusion",
|
|
"field_path": ["status_code"],
|
|
"value_labels": [
|
|
{"value": 0, "text": {"fr": "Pré-inclus", "en": "Pre-included"}},
|
|
{"value": 1, "text": {"fr": "Inclus", "en": "Included"}},
|
|
{"value": 2, "text": {"fr": "Exclus", "en": "Excluded"}}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Pattern 7: Pattern Matching in Multiple Fields
|
|
|
|
**Requirement:** Check if any medical note mentions specific condition
|
|
|
|
```json
|
|
{
|
|
"field_group": "Medical",
|
|
"field_name": "Mentions_Hypertension",
|
|
"source_name": "Calculated",
|
|
"source_id": "search_in_fields_using_regex",
|
|
"field_path": [
|
|
"hypertension|high.*pressure|HBP",
|
|
"Medical_History",
|
|
"Current_Conditions",
|
|
"Medication_Notes"
|
|
]
|
|
}
|
|
```
|
|
|
|
### Pattern 8: Extracted Parenthetical Classification
|
|
|
|
**Requirement:** Extract diagnosis type from formatted text like "Disease (Type A)"
|
|
|
|
```json
|
|
{
|
|
"field_group": "Classification",
|
|
"field_name": "Diagnosis_Type",
|
|
"source_name": "Calculated",
|
|
"source_id": "extract_parentheses_content",
|
|
"field_path": ["Formatted_Diagnosis"]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue 1: "Invalid JSON format" Error
|
|
|
|
**Symptom:** Configuration validation fails with JSON parsing error
|
|
|
|
**Cause:** Malformed JSON in field_path, value_labels, or field_condition
|
|
|
|
**Solution:**
|
|
1. Open cell in JSON validator (jsonlint.com)
|
|
2. Verify all:
|
|
- Array brackets: `[...]`
|
|
- Object braces: `{...}`
|
|
- String quotes: `"..."`
|
|
- Commas between elements
|
|
3. Fix syntax errors
|
|
4. Re-run validation
|
|
|
|
**Example - WRONG:**
|
|
```json
|
|
["name", "address" ] // WRONG: no comma after "name"
|
|
|
|
["name", "address"] // CORRECT
|
|
```
|
|
|
|
### Issue 2: Field Returns "undefined"
|
|
|
|
**Symptom:** Field value always "undefined" in output
|
|
|
|
**Causes:**
|
|
1. Field path doesn't match actual data structure
|
|
2. Questionnaire ID incorrect
|
|
3. Source type mismatch
|
|
|
|
**Solution:**
|
|
1. Check if source data exists in endobest_inclusions_old.json
|
|
2. Verify JSON path by stepping through manually
|
|
3. Check questionnaire ID (use `q_id` for fastest lookup)
|
|
4. Enable debug mode to see detailed errors
|
|
|
|
```bash
|
|
python eb_dashboard.py --debug
|
|
```
|
|
|
|
### Issue 3: Empty Array Result
|
|
|
|
**Symptom:** Wildcard path returns empty array instead of values
|
|
|
|
**Causes:**
|
|
1. Array elements don't exist at specified path
|
|
2. Wildcard position incorrect in path
|
|
|
|
**Solution:**
|
|
1. Verify array exists in source data
|
|
2. Check array element structure
|
|
3. Test path manually in JSON tool
|
|
|
|
**Example:**
|
|
```json
|
|
// WRONG: No elements at this path
|
|
["record", "items", "*", "nonexistent_field"]
|
|
|
|
// CORRECT: Match actual structure
|
|
["record", "items", "*", "existing_field"]
|
|
```
|
|
|
|
### Issue 4: Calculated Field Returns Error
|
|
|
|
**Symptom:** Calculated field value starts with "$$$$ "
|
|
|
|
**Causes:**
|
|
1. Function name wrong
|
|
2. Function argument count mismatch
|
|
3. Referenced fields not yet processed
|
|
|
|
**Solution:**
|
|
1. Check function name spelling
|
|
2. Verify argument count in field_path
|
|
3. Ensure referenced fields are defined BEFORE calculated field
|
|
4. Check for circular dependencies
|
|
|
|
**Common Errors:**
|
|
```
|
|
"$$$$ Unknown Custom Function: typo_name"
|
|
→ Check function name spelling
|
|
|
|
"$$$$ Argument Error: function requires N arguments"
|
|
→ Check field_path array length
|
|
|
|
"$$$$ Value Error: undefined"
|
|
→ Referenced field is undefined; check order in config
|
|
```
|
|
|
|
### Issue 5: value_labels Not Applied
|
|
|
|
**Symptom:** Raw value shown instead of mapped label
|
|
|
|
**Causes:**
|
|
1. Raw value doesn't match any entry in value_labels
|
|
2. JSON syntax error in value_labels
|
|
3. Case sensitivity mismatch
|
|
|
|
**Solution:**
|
|
1. Check raw value type (string vs. number)
|
|
2. Verify exact match in value_labels
|
|
3. Check for case mismatches (e.g., "Active" vs "active")
|
|
4. Add wildcard entry if needed
|
|
|
|
**Example:**
|
|
```json
|
|
{
|
|
"value_labels": [
|
|
{"value": "active", "text": {"fr": "Actif"}},
|
|
{"value": "inactive", "text": {"fr": "Inactif"}},
|
|
{"value": "*", "text": {"fr": "Autre"}} // Catch-all for unmapped values
|
|
]
|
|
}
|
|
```
|
|
|
|
### Issue 6: Performance Degradation After Adding Field
|
|
|
|
**Symptom:** Collection takes significantly longer after adding field
|
|
|
|
**Causes:**
|
|
1. Sequential questionnaire search (use q_id instead)
|
|
2. Expensive regex in search_in_fields_using_regex
|
|
3. Deep wildcard paths (multiple levels)
|
|
|
|
**Solution:**
|
|
1. Use `q_id=` instead of `q_name=` or `q_category=`
|
|
2. Simplify regex patterns
|
|
3. Flatten wildcard paths where possible
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
The Field Mapping Configuration provides:
|
|
|
|
✅ **100% Externalized:** No code changes needed to add fields
|
|
✅ **Flexible Sourcing:** Support for questionnaires, records, requests, calculated fields
|
|
✅ **Rich Transformations:** Labels, templates, conditions, custom functions
|
|
✅ **User-Friendly:** Excel-based configuration with validation
|
|
✅ **Performance Optimized:** Single-call questionnaire fetching, field batching
|
|
|
|
This architecture enables rapid iteration on data extraction without deploying code changes.
|
|
|
|
---
|
|
|
|
**Document End**
|