Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9877d50
Add inline `compared` syntax for N-way set-based output variable comp…
RakeshBobba03 Dec 16, 2025
7240f06
bug fixes and optimization
RakeshBobba03 Dec 16, 2025
71cc690
Merge branch 'main' into 1274-Comparison-in-Reporting
RakeshBobba03 Dec 16, 2025
13c2b2e
Documentation and test updates
RakeshBobba03 Dec 16, 2025
632539e
Merge branch 'main' into 1274-Comparison-in-Reporting
RakeshBobba03 Dec 16, 2025
96eb7eb
Merge branch 'main' into 1274-Comparison-in-Reporting
RamilCDISC Dec 19, 2025
a208474
Merge branch 'main' into 1274-Comparison-in-Reporting
RakeshBobba03 Dec 26, 2025
ec20481
Merge branch '1274-Comparison-in-Reporting' of https://github.com/cdi…
RakeshBobba03 Dec 26, 2025
461f7c4
Auto-updated branch with latest changes from main
SFJohnson24 Jan 12, 2026
b9764d3
Auto-updated branch with latest changes from main
SFJohnson24 Jan 12, 2026
51e9167
Auto-updated branch with latest changes from main
SFJohnson24 Jan 12, 2026
3ca300c
Auto-updated branch with latest changes from main
SFJohnson24 Jan 12, 2026
182af83
Auto-updated branch with latest changes from main
SFJohnson24 Jan 12, 2026
9a8d237
Auto-updated branch with latest changes from main
SFJohnson24 Jan 12, 2026
d35fc59
Auto-updated branch with latest changes from main
SFJohnson24 Jan 12, 2026
c9ea81b
Auto-updated branch with latest changes from main
SFJohnson24 Jan 13, 2026
b5dbc8b
Auto-updated branch with latest changes from main
SFJohnson24 Jan 13, 2026
0ed4a56
Auto-updated branch with latest changes from main
SFJohnson24 Jan 13, 2026
817f0de
Auto-updated branch with latest changes from main
SFJohnson24 Jan 14, 2026
0b88329
Auto-updated branch with latest changes from main
SFJohnson24 Jan 16, 2026
d04d9fc
Merge branch 'main' into 1274-Comparison-in-Reporting
RakeshBobba03 Jan 16, 2026
73addc0
Merge branch '1274-Comparison-in-Reporting' of https://github.com/cdi…
RakeshBobba03 Jan 16, 2026
82f819f
Fix USUBJID extraction and update error message to 'not available in …
RakeshBobba03 Jan 19, 2026
9da0602
Reverted the order-preserving changes
RakeshBobba03 Jan 19, 2026
80f7094
Auto-updated branch with latest changes from main
SFJohnson24 Jan 19, 2026
b4a60d0
Auto-updated branch with latest changes from main
SFJohnson24 Jan 20, 2026
b1ccce0
Auto-updated branch with latest changes from main
SFJohnson24 Jan 22, 2026
1c75a5f
Auto-updated branch with latest changes from main
SFJohnson24 Jan 26, 2026
f09ded5
Auto-updated branch with latest changes from main
SFJohnson24 Jan 26, 2026
db64b3b
Auto-updated branch with latest changes from main
SFJohnson24 Jan 27, 2026
e9cc29f
Auto-updated branch with latest changes from main
SFJohnson24 Jan 27, 2026
5423512
Auto-updated branch with latest changes from main
SFJohnson24 Jan 28, 2026
3b9c2a4
Auto-updated branch with latest changes from main
SFJohnson24 Jan 28, 2026
ec74f67
Merge branch 'main' into 1274-Comparison-in-Reporting
RakeshBobba03 Jan 30, 2026
442cd83
Merge branch '1274-Comparison-in-Reporting' of https://github.com/cdi…
RakeshBobba03 Jan 30, 2026
2d3facd
Modified _extract_comparison_metadata()
RakeshBobba03 Jan 30, 2026
b10549b
Auto-updated branch with latest changes from main
SFJohnson24 Jan 30, 2026
9e7a120
Merge branch 'main' into 1274-Comparison-in-Reporting
RakeshBobba03 Feb 4, 2026
8b917b6
fix: make extract_target_names_from_rule return list instead of set a…
RakeshBobba03 Feb 4, 2026
c4612e4
Merge branch 'main' into 1274-Comparison-in-Reporting
RakeshBobba03 Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 72 additions & 7 deletions cdisc_rules_engine/models/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
SOURCE_ROW_NUMBER,
)
from cdisc_rules_engine.enums.sensitivity import Sensitivity
from cdisc_rules_engine.enums.rule_types import RuleTypes
from cdisc_rules_engine.models.sdtm_dataset_metadata import SDTMDatasetMetadata
from cdisc_rules_engine.models.dataset_variable import DatasetVariable
from cdisc_rules_engine.models.validation_error_container import (
Expand Down Expand Up @@ -89,6 +90,21 @@ def _get_target_names_from_list_values(
existing.add(value)
return expanded

def _get_missing_variable_message(self) -> str:
"""Get appropriate message for missing variables based on rule type."""
rule_type = self.rule.get("rule_type", "")
metadata_check_types = [
RuleTypes.VARIABLE_METADATA_CHECK.value,
RuleTypes.VARIABLE_METADATA_CHECK_AGAINST_DEFINE.value,
RuleTypes.VARIABLE_METADATA_CHECK_AGAINST_DEFINE_XML_AND_LIBRARY.value,
RuleTypes.VARIABLE_METADATA_CHECK_AGAINST_LIBRARY.value,
RuleTypes.DATASET_METADATA_CHECK.value,
RuleTypes.DATASET_METADATA_CHECK_AGAINST_DEFINE.value,
]
if rule_type in metadata_check_types:
return "not available in metadata context"
return "Not in dataset"

def generate_targeted_error_object( # noqa: C901
self, targets: Iterable[str], data: pd.DataFrame, message: str
) -> ValidationErrorContainer:
Expand Down Expand Up @@ -143,8 +159,9 @@ def generate_targeted_error_object( # noqa: C901

if self.rule.get("sensitivity") == Sensitivity.DATASET.value:
# Only generate one error for rules with dataset sensitivity
missing_var_msg = self._get_missing_variable_message()
missing_vars = {
target: "Not in dataset" for target in targets_not_in_dataset
target: missing_var_msg for target in targets_not_in_dataset
}

# Create the initial error
Expand Down Expand Up @@ -220,6 +237,8 @@ def generate_targeted_error_object( # noqa: C901
errors_list = self._generate_errors_by_target_presence(
data, targets_not_in_dataset, all_targets_missing, errors_df
)

compare_groups = self._extract_comparison_metadata(self.rule)
return ValidationErrorContainer(
domain=(
f"SUPP{self.dataset_metadata.rdomain}"
Expand All @@ -232,6 +251,7 @@ def generate_targeted_error_object( # noqa: C901
targets=targets_list,
errors=errors_list,
message=message.replace("--", self.dataset_metadata.domain_cleaned or ""),
compare_groups=compare_groups,
)

def _generate_errors_by_target_presence(
Expand All @@ -254,14 +274,15 @@ def _generate_errors_by_target_presence(
Returns:
List of ValidationErrorEntity objects
"""
missing_vars = {target: "Not in dataset" for target in targets_not_in_dataset}
missing_var_msg = self._get_missing_variable_message()
missing_vars = {target: missing_var_msg for target in targets_not_in_dataset}

if all_targets_missing:
errors_list = []
for idx, row in data.iterrows():
error = ValidationErrorEntity(
value={
target: "Not in dataset" for target in targets_not_in_dataset
target: missing_var_msg for target in targets_not_in_dataset
},
dataset=self._get_dataset_name(pd.DataFrame([row])),
row=int(row.get(SOURCE_ROW_NUMBER, idx + 1)),
Expand Down Expand Up @@ -383,17 +404,16 @@ def _build_complete_error_value(
errors_df,
):
"""Build complete error value with all components."""
missing_var_msg = self._get_missing_variable_message()
if all_targets_missing:
error_value = {
target: "Not in dataset" for target in targets_not_in_dataset
}
error_value = {target: missing_var_msg for target in targets_not_in_dataset}
else:
error_value = self._build_error_value_from_row(first_row_idx, errors_df)
error_value = self._add_group_keys_to_error_value(
error_value, group_keys, grouping_variables
)

missing_vars = {target: "Not in dataset" for target in targets_not_in_dataset}
missing_vars = {target: missing_var_msg for target in targets_not_in_dataset}
if missing_vars:
error_value = {**error_value, **missing_vars}

Expand Down Expand Up @@ -509,6 +529,51 @@ def extract_target_names_from_value_level_metadata(self) -> List[str]:
ordered.append(name)
return ordered

def _extract_comparison_metadata(self, rule: dict) -> Optional[List[List[str]]]:
"""
Extract comparison metadata from rule's output_variables.

Supports mixed lists with inline `compared` blocks, e.g.:
Output Variables:
- $sibling_1
- compared:
- $child_A
- $child_B
- $child_C

Returns:
List of comparison groups (each group is a list of variable names),
or None if no comparison groups are defined.
"""
if "_cached_compare_groups" in rule:
return rule["_cached_compare_groups"]

output_variables = rule.get("output_variables", []) or []

flattened: List[str] = []
comparison_groups: List[List[str]] = []

for item in output_variables:
if isinstance(item, dict) and "compared" in item:
children = item.get("compared", [])
if isinstance(children, list):
valid_children = [c for c in children if isinstance(c, str)]
flattened.extend(valid_children)
if len(valid_children) >= 2:
comparison_groups.append(valid_children)
elif isinstance(item, str):
flattened.append(item)

result = comparison_groups if comparison_groups else None
rule["_cached_compare_groups"] = result

if flattened:
current_vars = rule.get("output_variables", [])
if any(isinstance(item, dict) for item in current_vars):
rule["output_variables"] = flattened

return result

@staticmethod
def _sequence_exists(sequence: pd.Series, row_name: Hashable) -> bool:
return (
Expand Down
8 changes: 6 additions & 2 deletions cdisc_rules_engine/models/validation_error_container.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional
from dataclasses import dataclass, field
from cdisc_rules_engine.utilities.utils import get_execution_status

Expand All @@ -18,13 +18,14 @@ class ValidationErrorContainer(BaseValidationEntity):
message: str | None = None
status: str | None = None
entity: str | None = None
compare_groups: Optional[List[List[str]]] = None

@property
def executionStatus(self):
return self.status or get_execution_status(self.errors)

def to_representation(self) -> dict:
return {
result = {
"executionStatus": self.executionStatus,
"dataset": self.dataset,
"domain": self.domain,
Expand All @@ -33,3 +34,6 @@ def to_representation(self) -> dict:
"errors": [error.to_representation() for error in self.errors],
**({"entity": self.entity} if self.entity else {}),
}
if self.compare_groups:
result["compare_groups"] = self.compare_groups
return result
7 changes: 6 additions & 1 deletion cdisc_rules_engine/services/reporting/base_report_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ def process_values(values: list[str]) -> list[str]:
if value is None:
processed_values.append("null")
continue
value = value.strip()
if isinstance(value, str) and "\n" in value:
lines = value.split("\n")
stripped_lines = [line.rstrip() for line in lines]
value = "\n".join(stripped_lines).strip()
elif isinstance(value, str):
value = value.strip()
if value == "" or value.lower() == "nan":
processed_values.append("null")
else:
Expand Down
12 changes: 10 additions & 2 deletions cdisc_rules_engine/services/reporting/excel_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,17 @@ def excel_update_worksheet(ws, rows, align_params=None, fill_empty_rows=False):
ws.cell(row=row_data.row, column=2).value = row_data.value
else:
for col_num, col_data in enumerate(row_data.values(), 1):
ws.cell(row=row_num, column=col_num).value = stringify_list(col_data)
cell_value = stringify_list(col_data)
ws.cell(row=row_num, column=col_num).value = cell_value
if align_params:
alignment_params = align_params.copy()
else:
alignment_params = {}
if isinstance(cell_value, str) and "\n" in cell_value:
alignment_params["wrap_text"] = True
alignment_params["vertical"] = "top"
ws.cell(row=row_num, column=col_num).alignment = Alignment(
**align_params
**alignment_params
)
if fill_empty_rows and (row_data[1] == "" or row_data[1] is None):
# Codelist is empty for Code Rows. Change background color
Expand Down
Loading
Loading