Skip to content

ecrf importer converter - #31

Open
rkrenn wants to merge 3 commits into
masterfrom
cursor/ecrf-importer-converter
Open

ecrf importer converter#31
rkrenn wants to merge 3 commits into
masterfrom
cursor/ecrf-importer-converter

Conversation

@rkrenn

@rkrenn rkrenn commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added support for converting Interfast3 laboratory CSV files into structured XLSX workbooks.
    • Added a conversion step to the eCRF import workflow, selectable through the command line.
    • Added dynamic converter support for future file formats.
    • Added tools for retrieving and serializing eCRF field options.
  • Bug Fixes
    • Improved handling of single-stream files such as CSVs during processing.
  • Import Improvements
    • Converted files can now flow directly into subsequent eCRF import steps.

rkrenn and others added 2 commits July 29, 2026 17:31
Support --task=convert with --converter to normalize input (e.g. Interfast3 lab CSV) into EcrfDataHorizontal intermediate files before import.

Co-authored-by: Cursor <cursoragent@cursor.com>
Match columns by eCRF field externalId, add CompleteEcrfField client, and let CSV FileProcessors report a single sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rkrenn, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2cfebfce-3f43-4b2f-9799-d473528faea9

📥 Commits

Reviewing files that changed from the base of the PR and between 8738255 and 5c2de66.

📒 Files selected for processing (1)
  • CTSMS/BulkProcessor/RestConnector.pm
📝 Walkthrough

Walkthrough

Adds pluggable eCRF file conversion, an Interfast3 lab CSV-to-XLSX converter, REST-based eCRF field lookup, CLI conversion-task support, and a single-stream sheet-name contract.

Changes

eCRF conversion

Layer / File(s) Summary
Converter loading API
CTSMS/BulkProcessor/Projects/ETL/EcrfImport.pm
Exports convert_ecrf_data, dynamically loads converter modules, validates convert or process, and returns intermediate output files.
Complete eCRF field lookup
CTSMS/BulkProcessor/RestRequests/ctsms/shared/ToolsService/CompleteEcrfField.pm
Adds REST querying, response conversion, item construction, and {value, label} serialization for completed eCRF fields.
Interfast3 lab conversion
CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm
Converts Interfast3 CSV rows into mapped XLSX lab worksheets using trial configuration, REST lookups, normalized headers, and row-level error handling.
CLI conversion task
CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/process.pl
Adds the convert task and --converter option, invokes conversion, validates output, and passes the intermediate file to later import steps.
Single-stream sheet contract
CTSMS/BulkProcessor/FileProcessor.pm
Adds get_sheet_names, returning one undefined entry for formats without worksheets.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant EcrfImport
  participant Interfast3LabData
  participant CTMS REST API
  participant XLSX
  CLI->>EcrfImport: convert_ecrf_data(file, converter)
  EcrfImport->>Interfast3LabData: convert(input file)
  Interfast3LabData->>CTMS REST API: fetch trial, field, and proband data
  CTMS REST API-->>Interfast3LabData: return lookup results
  Interfast3LabData->>XLSX: write mapped worksheet rows
  XLSX-->>CLI: return intermediate filename
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and directly reflects the main change: adding an eCRF importer converter.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/ecrf-importer-converter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Stop falling back to the sub claim in get_username_from_jwt.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CTSMS/BulkProcessor/Projects/ETL/EcrfImport.pm`:
- Around line 158-194: Update _load_converter to validate $spec as a safe Perl
package name before converting it into a filesystem path, rejecting traversal
segments or other invalid shapes before constructing $module_file. Remove the
global `@INC` mutation and require the validated absolute module path directly,
while preserving the existing file-existence check and converter-method
validation.

In
`@CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm`:
- Around line 258-296: Update _create_lab_sheets so each external_id is
normalized with the same _normalize_colnames transformation used for CSV headers
before looking it up in %csv_index_by_lc. Preserve the existing case-insensitive
matching and matched-column metadata, ensuring punctuation differences such as
slashes resolve to the normalized CSV column.

In `@CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/process.pl`:
- Around line 285-288: Update the conversion result validation in the eval block
around convert_ecrf_data so success requires $outfile to identify a regular,
readable, non-empty file, not merely a non-empty string. Preserve the existing
failure path for invalid output and only assign/pass the path to
import_ecrf_data_horizontal when this validation succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4dd1e3dc-a42c-42d7-907a-7503a6a9a665

📥 Commits

Reviewing files that changed from the base of the PR and between ed9276b and 8738255.

📒 Files selected for processing (5)
  • CTSMS/BulkProcessor/FileProcessor.pm
  • CTSMS/BulkProcessor/Projects/ETL/EcrfImport.pm
  • CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm
  • CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/process.pl
  • CTSMS/BulkProcessor/RestRequests/ctsms/shared/ToolsService/CompleteEcrfField.pm

Comment on lines +158 to +194
sub convert_ecrf_data {
my ($file,$converter) = @_;

my $convert_code = _load_converter($converter);
my $infile = get_input_filename($file,$ecrf_import_filename);
my $outfile = &$convert_code($infile);
if (length($outfile)) {
scriptinfo("converter '$converter' wrote intermediate file $outfile",getlogger(__PACKAGE__));
}
return $outfile;
}

sub _load_converter {
my ($spec) = @_;
scripterror('converter module required (e.g. --converter=Converter::MyConverter)',getlogger(getscriptpath()))
unless length($spec);

# Converters live next to process.pl: EcrfImporter/Converter/*.pm
my $importer_dir = Cwd::abs_path(File::Basename::dirname(__FILE__) . '/EcrfImporter');
(my $rel_path = $spec) =~ s|::|/|g;
my $module_file = $importer_dir . '/' . $rel_path . '.pm';
scripterror("converter module not found: $module_file",getlogger(getscriptpath()))
unless -f $module_file;

unshift(@INC,$importer_dir) unless grep { $_ eq $importer_dir } @INC;
eval {
require $rel_path . '.pm';
1;
} or do {
scripterror("failed to load converter '$spec': " . ($@ // 'unknown error'),getlogger(getscriptpath()));
};

my $convert_code = $spec->can('convert') || $spec->can('process');
scripterror("converter '$spec' must expose convert() or process()",getlogger(getscriptpath()))
unless $convert_code;
return $convert_code;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validate converter specs before require and avoid mutating global @INC.

$spec is converted into a filesystem path with simple ::/ substitution and then required. A spec like ..::..::..::somewhere::payload can resolve outside EcrfImporter/Converter/ and be required if .pm exists at that path. Reject non-package-name shapes before building $module_file, and use the already-validated absolute path directly instead of pushing/importing EcrfImporter into @INC.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CTSMS/BulkProcessor/Projects/ETL/EcrfImport.pm` around lines 158 - 194,
Update _load_converter to validate $spec as a safe Perl package name before
converting it into a filesystem path, rejecting traversal segments or other
invalid shapes before constructing $module_file. Remove the global `@INC` mutation
and require the validated absolute module path directly, while preserving the
existing file-existence check and converter-method validation.

Comment on lines +258 to +296
my %csv_index_by_lc = ();
for (my $i = 0; $i < scalar @$csv_colnames; $i++) {
$csv_index_by_lc{lc($csv_colnames->[$i])} = $i;
}

$context->{sheets_by_name} = {};
$context->{anfonr_index} = $csv_index_by_lc{lc('AnfoNr')};

foreach my $ecrf (@{$context->{lab_ecrfs}}) {
my $name = $ecrf->{name};
my @header = ('proband_id');
my @matched = ();
foreach my $col (@{$context->{lab_columns_by_name}->{$name} // []}) {
my $csv_index = $csv_index_by_lc{lc($col->{external_id})};
next unless defined $csv_index;
push(@header,$col->{colname});
push(@matched,{
colname => $col->{colname},
csv_index => $csv_index,
external_id => $col->{external_id},
});
}

my $sheetname = sanitize_spreadsheet_name($name);
my $worksheet = $context->{workbook}->add_worksheet($sheetname);
for (my $c = 0; $c < scalar @header; $c++) {
$worksheet->write_string(0,$c,$header[$c],$context->{header_format});
}

$context->{sheets_by_name}->{$name} = {
worksheet => $worksheet,
header => \@header,
matched => \@matched,
next_row => 1,
ecrf => $ecrf,
};
processing_info($context->{tid},"sheet '$sheetname': " . (scalar @matched) . ' matched column(s)',getlogger(__PACKAGE__));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -a 'CSVDB.pm'
rg -n 'sub sanitize_column_name' -A 20 $(fd -a 'CSVDB.pm')

Repository: phoenixctms/bulk-processor

Length of output: 706


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== imports and relevant subroutines =="
sed -n '1,80p' CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm
sed -n '220,310p' CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm
sed -n '390,460p' CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm

echo "== sanitize_column_name occurrences =="
rg -n 'sanitize_column_name|normalize_colnames|AnfoNr|external_id|colname' CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm

echo "== standalone Perl behavior probe for comparison transform =="
perl -Mstrict -Mwarnings -e '
sub sanitize_column_name {
    my $column_name = shift;
    $column_name =~ s/\\W/_/g;
    return $column_name;
}
my `@raw` = ("Proband ID","Creatinine","Glucose_mg/dL","HCT%","WBC");
my `@sanitized` = map { sanitize_column_name($_) } `@raw`;
my %csv_index_by_lc_sanitized = ();
for (my $i = 0; $i < scalar `@sanitized`; $i++) {
    $csv_index_by_lc_sanitized{lc($sanitized[$i])} = $i;
}
foreach my $config ("Proband ID","Creatinine","Glucose_mg/dL","HCT%","WBC") {
    my $csv_index = $csv_index_by_lc_sanitized{lc($config)};
    print "raw=$config csv_index=$csv_index\n";
}
'

Repository: phoenixctms/bulk-processor

Length of output: 9617


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lab_columns_by_name population =="
sed -n '120,215p' CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm

echo "== CSVDB sanitize_column_name implementation =="
sed -n '80,105p' CTSMS/BulkProcessor/SqlConnectors/CSVDB.pm

Repository: phoenixctms/bulk-processor

Length of output: 4543


Normalize both CSV headers and external_id before matching columns

_normalize_colnames() replaces every non-word header character with _ before filling %csv_index_by_lc, but _create_lab_sheets() looks up raw external_id values. Headers like creatinine_mg/dL become creatinine_mg_dL; if the eCRF field external_id is the original analyte title, it will never find its CSV column and the mapped data is skipped silently. Apply the same transform to external_id before lookup, or compare against the raw trimmed header instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/Converter/Interfast3LabData.pm`
around lines 258 - 296, Update _create_lab_sheets so each external_id is
normalized with the same _normalize_colnames transformation used for CSV headers
before looking it up in %csv_index_by_lc. Preserve the existing case-insensitive
matching and matched-column metadata, ensuring punctuation differences such as
slashes resolve to the normalized CSV column.

Comment on lines +285 to +288
eval {
$outfile = convert_ecrf_data($file,$converter);
$result = length($outfile) ? 1 : 0;
if ($result) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate that conversion produced a usable file.

length($outfile) only verifies that the converter returned a non-empty string. A missing, unreadable, or zero-byte path will be reported as successful, assigned to $file, and passed to import_ecrf_data_horizontal. Check that the output is a regular, readable, non-empty file before returning success.

Proposed fix
-        $result = length($outfile) ? 1 : 0;
+        $result = (
+            defined($outfile)
+            && length($outfile)
+            && -f($outfile)
+            && -r($outfile)
+            && -s($outfile)
+        ) ? 1 : 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
eval {
$outfile = convert_ecrf_data($file,$converter);
$result = length($outfile) ? 1 : 0;
if ($result) {
eval {
$outfile = convert_ecrf_data($file,$converter);
$result = (
defined($outfile)
&& length($outfile)
&& -f($outfile)
&& -r($outfile)
&& -s($outfile)
) ? 1 : 0;
if ($result) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CTSMS/BulkProcessor/Projects/ETL/EcrfImporter/process.pl` around lines 285 -
288, Update the conversion result validation in the eval block around
convert_ecrf_data so success requires $outfile to identify a regular, readable,
non-empty file, not merely a non-empty string. Preserve the existing failure
path for invalid output and only assign/pass the path to
import_ecrf_data_horizontal when this validation succeeds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant