Skip to content

HTML-883: add personAttribute tag for viewing/entering/editing person attributes#334

Merged
mogoodrich merged 14 commits into
masterfrom
feature/person-attribute-tag
May 29, 2026
Merged

HTML-883: add personAttribute tag for viewing/entering/editing person attributes#334
mogoodrich merged 14 commits into
masterfrom
feature/person-attribute-tag

Conversation

@mogoodrich

@mogoodrich mogoodrich commented May 27, 2026

Copy link
Copy Markdown
Member

mogoodrich and others added 2 commits May 27, 2026 14:00
… attributes

Adds a new <personAttribute> tag that supports viewing, entering, and editing
person attributes within HTML forms. Supports String, Concept, and Location
PersonAttributeType formats.

Features:
- attributeType attribute (UUID of a PersonAttributeType) — required
- VIEW mode: displays the current attribute value (concept/location rendered
  by name, not raw ID)
- ENTER/EDIT modes: renders appropriate widget (TextFieldWidget for String,
  DropdownWidget for Concept/Location)
- Concept type: mandatory answerConceptIds (comma-separated IDs/UUIDs)
- Location type: optional tags attribute (comma-separated tag names) to filter
  the location dropdown
- When multiple non-voided attributes of the same type exist, the most recently
  created one is used and a warning is logged
- Blank submission voids an existing attribute

New files:
- PersonAttributeTagHandler.java — tag handler registered in moduleApplicationContext.xml
- PersonAttributeSubmissionElement.java — widget + submission logic
- PersonAttributeTagTest.java — 16 regression tests covering all cases
- 5 test form XML fixtures (String, Concept, Location, Location+tag, Location-filter-only)

Modified files:
- moduleApplicationContext.xml — registers 'personAttribute' handler
- RegressionTest-data-openmrs-2.8.xml — adds PersonAttributeType 19 (Location),
  a location attribute for patient 2, and two Civil Status attributes for
  patient 2 (for multiple-attribute warning tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mogoodrich
mogoodrich marked this pull request as draft May 27, 2026 18:22
@mogoodrich
mogoodrich marked this pull request as ready for review May 27, 2026 19:47
@mogoodrich
mogoodrich requested review from chibongho, cioan and mseaton May 27, 2026 19:49

private static final String FORMAT_CONCEPT = "org.openmrs.Concept";

private static final String FORMAT_LOCATION = "org.openmrs.Location";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These constants seem unnecessary when you can and should just get the name off the class object itself. If you want to keep the constants, assign them values based on XXX.class.getName()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fair. Fixed.

private PersonAttribute existingAttribute;

/** Format of the attribute type (java.lang.String / org.openmrs.Concept / org.openmrs.Location). */
private final String format;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you need to keep this here as something independent from PersonAttributeType? Can't you just do attributeType.getFormat() if you need it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fair. Over-optimization by Claude I should have fixed.

throws BadFormDesignException {

String answerConceptIds = parameters.get("answerConceptIds");
if (!StringUtils.hasText(answerConceptIds)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe PersonAttributeType has a foreignKey property that may be intended to act as a pointer to the allowed value set. eg. The foreignKey could point to a concept that is a question with answers (or possibly a set with members, not sure) and this should define the set of allowed answers. We should likely support that here so that this handles the basics as expected.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, fair, I had forgotten about this. I will fix. Arguably, we shouldn't even allow answerConceptIds, but I will keep that since it's been created.

// Create a new attribute
PersonAttribute newAttribute = new PersonAttribute();
newAttribute.setAttributeType(attributeType);
newAttribute.setValue(value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not 100% sure this is the right way to set the value, can you just double check? Seems like this will set the primary key value of the concept or location if those are the types? It would be better if we could call a method to set the value to the actual Concept or Location that we parse here, and then let core store that as a string value however it sees fit, to protect against any changes in the future, but if this is the way to do it, it's fine I guess

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. The "seralize" method below wasn't something I was familiar, but I double-checked and Claude is indeed correct.

Claude: The reviewer's concern is fully valid and there's a better API available.

The key findings:

  • PersonAttribute.setValue(String) is the only setter ? no typed API
  • But both Concept and Location implement Attributable, which has a serialize() method
  • Concept.serialize() ? "" + getConceptId(); Location.serialize() ? "" + getLocationId()
  • getHydratedObject() uses hydrate(getValue()) to reconstruct the typed object
  • The reviewer is right: we should parse the submitted value to the actual object and call serialize() on it, letting core determine the storage format

Currently we pass the raw widget string directly. The fix is to look up the typed object and call serialize() ? exactly as Attributable intends.

mogoodrich and others added 11 commits May 29, 2026 10:01
… attributes

(use seralization methods to set values)
…upport

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…widget support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… widget population

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…swer population

When answerConceptIds is not specified on the tag, the concept dropdown now
resolves its options from PersonAttributeType.foreignKey: if the referenced
concept is a set, its non-retired members are used; otherwise the concept's
non-retired answers are used. answerConceptIds still takes precedence when present.
…dropdown

Aligns with ObsSubmissionElement's getName(Context.getLocale(), false) pattern
rather than the no-arg getName() to ensure correct locale resolution.

@mogoodrich mogoodrich left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated per code review (and one other inconsistency I found).

fyi @mseaton if you want to take another look.

I am going to test and then merge.


private static final String FORMAT_CONCEPT = "org.openmrs.Concept";

private static final String FORMAT_LOCATION = "org.openmrs.Location";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fair. Fixed.

private PersonAttribute existingAttribute;

/** Format of the attribute type (java.lang.String / org.openmrs.Concept / org.openmrs.Location). */
private final String format;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fair. Over-optimization by Claude I should have fixed.

throws BadFormDesignException {

String answerConceptIds = parameters.get("answerConceptIds");
if (!StringUtils.hasText(answerConceptIds)) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, fair, I had forgotten about this. I will fix. Arguably, we shouldn't even allow answerConceptIds, but I will keep that since it's been created.

// Create a new attribute
PersonAttribute newAttribute = new PersonAttribute();
newAttribute.setAttributeType(attributeType);
newAttribute.setValue(value);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. The "seralize" method below wasn't something I was familiar, but I double-checked and Claude is indeed correct.

Claude: The reviewer's concern is fully valid and there's a better API available.

The key findings:

  • PersonAttribute.setValue(String) is the only setter ? no typed API
  • But both Concept and Location implement Attributable, which has a serialize() method
  • Concept.serialize() ? "" + getConceptId(); Location.serialize() ? "" + getLocationId()
  • getHydratedObject() uses hydrate(getValue()) to reconstruct the typed object
  • The reviewer is right: we should parse the submitted value to the actual object and call serialize() on it, letting core determine the storage format

Currently we pass the raw widget string directly. The fix is to look up the typed object and call serialize() ? exactly as Attributable intends.

@mogoodrich
mogoodrich merged commit 586acdc into master May 29, 2026
5 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.

2 participants