Skip to content

filter eCRF forms also by a field's external_id#404

Merged
rkrenn merged 2 commits intomasterfrom
filter_by_ext_id
Dec 13, 2025
Merged

filter eCRF forms also by a field's external_id#404
rkrenn merged 2 commits intomasterfrom
filter_by_ext_id

Conversation

@rkrenn
Copy link
Copy Markdown
Collaborator

@rkrenn rkrenn commented Dec 13, 2025

Summary by CodeRabbit

  • New Features

    • Expanded search capabilities now include external identifiers, enabling more flexible field discovery across the system.
    • Improved input field identification and display through enhanced handling of external identifiers.
  • Refactor

    • Internal restructuring of input field models to improve code organization and maintainability.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 13, 2025

Walkthrough

The changes refactor ECRF field search logic and input field naming by introducing external ID support. Search criteria now include externalId fields with explicit null/empty guards. Input field models transition from public getUniqueName() methods to protected getInputExternalId() implementations, with getName() preferring externalId when available.

Changes

Cohort / File(s) Summary
Core search criteria
core/src/main/java/org/phoenixctms/ctsms/domain/ECRFFieldValueDaoImpl.java
Enhanced applyEcrfFieldSearchCriterions to include externalId fields with explicit null/empty guards alongside existing nameL10nKey and titleL10nKey ilike operations.
Input model base and hierarchy
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java, InputFieldConfig.java, InputFieldInVOConfig.java, InputFieldOutVOConfigBase.java
Added or modified getName() to prefer externalId over field name when available. Added protected abstract getInputExternalId() method. Removed/commented public getUniqueName() declarations.
Specialized input models
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/DummyInputModel.java, EcrfFieldInputModel.java, InquiryDummyInputModel.java, InquiryInputModel.java, ProbandListEntryTagInputModel.java
Updated getName() implementations to use super.getName() and added getInputExternalId() overrides returning model-specific external IDs. Removed/commented public getUniqueName() methods across all models.
Configuration (no-op change)
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldOutVOConfig.java
Added commented-out getExternalId() override; no functional behavior change.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Search criteria logic: The expanded null/empty guards and externalId inclusion in ECRFFieldValueDaoImpl.applyEcrfFieldSearchCriterions requires careful validation to ensure OR-disjunction behavior is preserved.
  • API signature removals: Multiple public getUniqueName() methods are removed/commented across the hierarchy; confirm no external consumers depend on this method.
  • getName() logic consistency: Verify that externalId fallback behavior is correctly implemented and consistent across all input model subclasses, particularly around null-safety and empty string handling.
  • InquiryInputModel changes: The introduction of a new overloaded getName(String) method with delegation patterns should be checked for correctness.

Possibly related PRs

Suggested reviewers

  • SigridDeller

Poem

🐰 External IDs now take the stage,
Names bow to a new golden age,
Where uniqueness fades to jest,
And externalId knows what's best,
Search criteria dance with guards so keen—
The smartest names the web has seen! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely describes the main objective: adding the ability to filter eCRF forms by a field's external_id. It accurately reflects the primary changes across multiple files that implement this filtering functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch filter_by_ext_id

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InquiryInputModel.java (1)

249-255: Remove commented-out code or deprecate the method properly.

Same issue as in ProbandListEntryTagInputModel.java: commented-out code should not remain in production. Either remove this entirely or properly deprecate with @Deprecated annotation.

Apply this diff to remove the commented code:

-	//	@Override
-	//	public String getUniqueName() {
-	//		if (inquiry != null) {
-	//			return inquiry.getUniqueName();
-	//		}
-	//		return null;
-	//	}

Alternatively, if deprecation is needed:

-	//	@Override
-	//	public String getUniqueName() {
-	//		if (inquiry != null) {
-	//			return inquiry.getUniqueName();
-	//		}
-	//		return null;
-	//	}
+	@Override
+	@Deprecated
+	public String getUniqueName() {
+		// Deprecated: Use getName() which now incorporates external ID
+		if (inquiry != null) {
+			return inquiry.getUniqueName();
+		}
+		return null;
+	}
🧹 Nitpick comments (6)
core/src/main/java/org/phoenixctms/ctsms/domain/ECRFFieldValueDaoImpl.java (2)

123-127: Simplify the double-nested AND and reconsider the guard logic.

The Restrictions.and(Restrictions.and(...)) nesting on lines 123-124 is unnecessary and reduces readability. Additionally, the guard requires BOTH externalId and inputField0.externalId to be empty/null before searching inputField0.nameL10nKey. This might be overly restrictive—consider whether the intended behavior is to search nameL10nKey whenever inputField0.externalId is empty, regardless of the parent externalId.

Apply this diff to flatten the nested AND:

-junction.add(Restrictions.and(
-    Restrictions.and(
-        Restrictions.or(Restrictions.eq("externalId", ""), Restrictions.isNull("externalId")),
-        Restrictions.or(Restrictions.eq("inputField0.externalId", ""), Restrictions.isNull("inputField0.externalId"))),
-    Restrictions.ilike("inputField0.nameL10nKey", fieldQuery, MatchMode.ANYWHERE)));
+junction.add(Restrictions.and(
+    Restrictions.or(Restrictions.eq("externalId", ""), Restrictions.isNull("externalId")),
+    Restrictions.or(Restrictions.eq("inputField0.externalId", ""), Restrictions.isNull("inputField0.externalId")),
+    Restrictions.ilike("inputField0.nameL10nKey", fieldQuery, MatchMode.ANYWHERE)));

122-133: Consider extracting guard logic into helper methods.

The repeated pattern of checking for null/empty fields (e.g., Restrictions.or(Restrictions.eq("externalId", ""), Restrictions.isNull("externalId"))) appears multiple times. Extracting this into a helper method would improve maintainability and reduce duplication.

For example, add a helper method:

private static Junction isNullOrEmpty(String propertyName) {
    return Restrictions.or(
        Restrictions.eq(propertyName, ""), 
        Restrictions.isNull(propertyName)
    );
}

Then refactor the guards:

-junction.add(Restrictions.and(
-    Restrictions.or(Restrictions.eq("externalId", ""), Restrictions.isNull("externalId")),
-    Restrictions.ilike("inputField0.externalId", fieldQuery, MatchMode.ANYWHERE)));
+junction.add(Restrictions.and(
+    isNullOrEmpty("externalId"),
+    Restrictions.ilike("inputField0.externalId", fieldQuery, MatchMode.ANYWHERE)));
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldOutVOConfig.java (1)

29-32: Commented code for future reference.

The commented-out getExternalId() override appears to be a placeholder for potential future functionality. While this documents the intent, consider whether these commented blocks should be tracked as TODOs or removed if not needed in the near term.

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldConfig.java (1)

15-15: Commented abstract method declaration.

Similar to other files in this PR, the commented-out abstract method serves as documentation but doesn't affect functionality. Consider consolidating these placeholder comments or tracking them through issue management.

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java (1)

359-359: Avoid commented-out public API; either remove it fully or deprecate properly

//public abstract String getUniqueName(); looks like a half-step and will rot. If migration is done, delete it; if not, consider a temporary @Deprecated concrete method (delegating to the old behavior) to make the transition explicit.

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/DummyInputModel.java (1)

186-189: Remove commented-out getUniqueName() block

Since the method is no longer part of the contract, keeping the commented override is just noise and makes future refactors harder.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5937774 and 16b1e90.

📒 Files selected for processing (11)
  • core/src/main/java/org/phoenixctms/ctsms/domain/ECRFFieldValueDaoImpl.java (1 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/DummyInputModel.java (2 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/EcrfFieldInputModel.java (3 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldConfig.java (1 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldInVOConfig.java (2 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldOutVOConfig.java (1 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldOutVOConfigBase.java (2 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java (2 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InquiryDummyInputModel.java (1 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InquiryInputModel.java (3 hunks)
  • web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/ProbandListEntryTagInputModel.java (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldInVOConfig.java (1)
common/src/main/java/org/phoenixctms/ctsms/util/CommonUtil.java (1)
  • CommonUtil (113-2756)
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java (1)
common/src/main/java/org/phoenixctms/ctsms/util/CommonUtil.java (1)
  • CommonUtil (113-2756)
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldOutVOConfigBase.java (1)
common/src/main/java/org/phoenixctms/ctsms/util/CommonUtil.java (1)
  • CommonUtil (113-2756)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and Test
🔇 Additional comments (12)
core/src/main/java/org/phoenixctms/ctsms/domain/ECRFFieldValueDaoImpl.java (1)

117-136: Verify the search behavior with integration tests.

The new search logic introduces complex precedence rules with multiple guards. Ensure that all intended search paths are covered by tests, including edge cases where various combinations of fields are null/empty.

Consider adding test cases for:

  • Searching when ECRFField.externalId is set vs. empty
  • Searching when InputField.externalId is set vs. empty
  • Searching when both externalId fields are set/empty
  • Verifying that nameL10nKey is searched only when both externalId fields are empty
  • Verifying that inputField0.titleL10nKey is searched when titleL10nKey is empty
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldOutVOConfigBase.java (1)

19-27: LGTM: External ID preference logic is correct.

The getName() implementation properly handles null checks and uses CommonUtil.isEmptyString() to determine whether to prefer the external ID over the field name. This aligns with the PR's objective to enable filtering by external ID.

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldInVOConfig.java (1)

28-35: LGTM: Consistent external ID preference implementation.

The getName() logic matches the pattern established in InputFieldOutVOConfigBase, correctly using CommonUtil.isEmptyString() to determine whether to prefer the external ID.

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/EcrfFieldInputModel.java (2)

168-177: LGTM: Message construction now uses external ID-aware naming.

The change from inputField.getName() to super.getName() ensures that the message titles will use the external ID when available, consistent with the PR's objective.


647-653: LGTM: External ID accessor correctly implemented.

The getInputExternalId() override properly checks for null before accessing ecrfField.getExternalId().

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InquiryDummyInputModel.java (1)

28-30: LGTM: Simplified delegation to parent class.

The getName() implementation now consistently delegates to the parent class with the appropriate message code. This aligns with the broader refactoring to centralize external ID-aware naming logic.

Note: This depends on a getName(String) overload in InquiryInputModel (mentioned in the AI summary but not included in this review).

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/ProbandListEntryTagInputModel.java (2)

551-558: LGTM!

The new getInputExternalId() method correctly provides external ID access with proper null safety. The protected visibility is appropriate for this hook method.


185-187: [rewritten comment]
[classification tag]

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InquiryInputModel.java (3)

163-172: LGTM!

The new protected getName(String newInquiryValueInputTitleCode) method is a good refactoring that parameterizes the message code and reduces duplication. Using super.getName() correctly delegates name resolution to the superclass.


174-177: LGTM!

The delegation pattern is clean and maintains the public API contract while consolidating the implementation logic.


534-541: LGTM!

The new getInputExternalId() method correctly implements the external ID hook pattern with proper null safety. The implementation is consistent with the parallel change in ProbandListEntryTagInputModel.java.

web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/DummyInputModel.java (1)

395-398: getInputExternalId() returning null is fine for a dummy model—verify it won’t break externalId-based filtering

If dummy inputs can appear in the filtered collections, ensure the new externalId-aware search logic handles null external IDs as intended (no accidental matches / no NPEs).

Comment on lines +230 to +239
protected abstract String getInputExternalId();

public String getName() {
String externalId = getInputExternalId();
if (CommonUtil.isEmptyString(externalId)) {
return super.getName();
} else {
return externalId;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and examine the class definition and parent class
find . -name "InputModel.java" -type f | head -5

Repository: phoenixctms/ctsms

Length of output: 146


🏁 Script executed:

# Get the full context around the methods mentioned
head -n 350 web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java | tail -n 150

Repository: phoenixctms/ctsms

Length of output: 5361


🏁 Script executed:

# Search for all usages of getName() on InputModel instances
rg "\.getName\(\)" --type java -B 2 -A 2 | head -100

Repository: phoenixctms/ctsms

Length of output: 11630


🏁 Script executed:

# Find the parent class of InputModel
head -n 50 web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java | grep -E "class InputModel|extends|implements"

Repository: phoenixctms/ctsms

Length of output: 129


🏁 Script executed:

# Search specifically for InputModel usages with getName()
rg "InputModel" --type java -B 3 -A 3 | grep -A 5 -B 5 "getName" | head -80

Repository: phoenixctms/ctsms

Length of output: 43


🏁 Script executed:

# Search for map/collection operations on InputModel that might use getName as a key
rg "InputModel" --type java -B 2 -A 2 | grep -E "(put|get|key|Map|contains|indexOf)" | head -40

Repository: phoenixctms/ctsms

Length of output: 5271


🏁 Script executed:

# Find InputFieldOutVOConfigBase class
find . -name "InputFieldOutVOConfigBase.java" -type f

Repository: phoenixctms/ctsms

Length of output: 161


🏁 Script executed:

# Examine the parent class
cat -n web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldOutVOConfigBase.java | head -100

Repository: phoenixctms/ctsms

Length of output: 1712


🏁 Script executed:

# Search for getName() being used specifically in context where it matters as an identifier
rg "getName\(\)" --type java -B 5 -A 1 | grep -E "(Map|put|get|Set|contains|key|id)" -B 8 -A 2 | head -100

Repository: phoenixctms/ctsms

Length of output: 13001


🏁 Script executed:

# Find InputFieldConfig class
find . -name "InputFieldConfig.java" -type f

Repository: phoenixctms/ctsms

Length of output: 152


🏁 Script executed:

# Check what InputFieldConfig defines
cat -n web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldConfig.java | head -80

Repository: phoenixctms/ctsms

Length of output: 3062


🏁 Script executed:

# Verify the exact override chain and whether InputModel.getName() is new
# Look at entire InputModel class to understand structure
wc -l web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java

Repository: phoenixctms/ctsms

Length of output: 148


🏁 Script executed:

# Check the git context to see if this is truly new behavior or refactoring
# Look for any comments or annotations in InputModel around line 230-239
sed -n '220,250p' web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java

Repository: phoenixctms/ctsms

Length of output: 722


🏁 Script executed:

# Check InputModel subclasses to see if they override getInputExternalId()
rg "getInputExternalId" --type java -B 2 -A 5

Repository: phoenixctms/ctsms

Length of output: 4535


🏁 Script executed:

# Final check: confirm parent class uses inputField directly
grep -A 8 "public String getName()" web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputFieldOutVOConfigBase.java

Repository: phoenixctms/ctsms

Length of output: 374


🏁 Script executed:

# Check if there's any usage of InputModel.getName() in templates or JS context where it might be used as ID
rg "getName\(\)" --type java web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/ -B 3 -A 3 | grep -E "(input|id|select|radio|checkbox|component)" -i

Repository: phoenixctms/ctsms

Length of output: 16840


Add @Override annotation to getName() method for compile-time safety

The method is overriding a parent class method but lacks the @Override annotation. Note that the externalId-preferring behavior already existed in the parent class InputFieldOutVOConfigBase, so this refactoring extracts externalId retrieval to an abstract hook (getInputExternalId()) allowing subclasses to provide their own sources—a design improvement rather than a breaking change.

-    public String getName() {
+    @Override
+    public String getName() {
         String externalId = getInputExternalId();
         if (CommonUtil.isEmptyString(externalId)) {
             return super.getName();
         } else {
             return externalId;
         }
     }
🤖 Prompt for AI Agents
In
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/InputModel.java
around lines 230 to 239, the getName() method overrides a superclass method but
lacks the @Override annotation; add the @Override annotation immediately above
the getName() method declaration (keeping the existing logic and signature) to
ensure compile-time checking that it correctly overrides the parent method.

Comment on lines +274 to +280
// @Override
// public String getUniqueName() {
// if (tag != null) {
// return tag.getUniqueName();
// }
// return null;
// }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove commented-out code or deprecate the method properly.

Commented-out code should not remain in production. If getUniqueName() is being removed as part of the API transition to external IDs, it should be either:

  1. Removed entirely, or
  2. Deprecated with @Deprecated annotation and a clear migration path in the javadoc

The commented-out code creates ambiguity and technical debt.

Apply this diff to remove the commented code:

-	//	@Override
-	//	public String getUniqueName() {
-	//		if (tag != null) {
-	//			return tag.getUniqueName();
-	//		}
-	//		return null;
-	//	}

Alternatively, if this needs a deprecation period, apply this diff instead:

-	//	@Override
-	//	public String getUniqueName() {
-	//		if (tag != null) {
-	//			return tag.getUniqueName();
-	//		}
-	//		return null;
-	//	}
+	@Override
+	@Deprecated
+	public String getUniqueName() {
+		// Deprecated: Use getName() which now incorporates external ID
+		if (tag != null) {
+			return tag.getUniqueName();
+		}
+		return null;
+	}
📝 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
// @Override
// public String getUniqueName() {
// if (tag != null) {
// return tag.getUniqueName();
// }
// return null;
// }
Suggested change
// @Override
// public String getUniqueName() {
// if (tag != null) {
// return tag.getUniqueName();
// }
// return null;
// }
@Override
@Deprecated
public String getUniqueName() {
// Deprecated: Use getName() which now incorporates external ID
if (tag != null) {
return tag.getUniqueName();
}
return null;
}
🤖 Prompt for AI Agents
In
web/src/main/java/org/phoenixctms/ctsms/web/model/shared/inputfield/ProbandListEntryTagInputModel.java
around lines 274-280, remove the commented-out getUniqueName() implementation to
avoid leaving dead code; either delete these commented lines entirely, or if a
deprecation period is required, restore the method as a real method annotated
with @Deprecated and add a Javadoc explaining the migration to external IDs
(point to the new getter to use and any timeline), then run formatter/compile to
ensure no unused imports or warnings remain.

@rkrenn rkrenn merged commit 046c867 into master Dec 13, 2025
2 checks passed
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