Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public boolean isEnabled() {
return true;
}

/** Always on (see isEnabled), so config UIs must not offer a toggle. */
@Override
public boolean isToggleable() {
return false;
}

@Override
protected FacilityInfo gatherData(Visit visit) {
if (visit.getLocation() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public boolean isEnabled() {
return true;
}

/** Always on (see isEnabled), so config UIs must not offer a toggle. */
@Override
public boolean isToggleable() {
return false;
}

@Override
protected FooterInfo gatherData(Visit visit) {
User currentUser = Context.getAuthenticatedUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public boolean isEnabled() {
return true;
}

/** Always on (see isEnabled), so config UIs must not offer a toggle. */
@Override
public boolean isToggleable() {
return false;
}

@Override
protected PatientVisitInfo gatherData(Visit visit) {
Patient patient = visit.getPatient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
package org.openmrs.module.patientdocuments.api.section;

import org.openmrs.Visit;
import org.openmrs.api.context.Context;
import org.openmrs.module.patientdocuments.common.PatientDocumentsConstants;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

Expand All @@ -19,6 +21,8 @@
* renderXml() on each enabled section sorted by getOrder().
* Implementing directly (without TypedSection) is valid for sections that don't
* fit the gather-then-render flow.
* Config UIs list the same beans via getLabel(), isEnabled(), getOrder() and
* isToggleable() — sections need no extra registration to appear there.
*/
public interface VisitSummarySection {

Expand All @@ -44,11 +48,59 @@ default int getOrder() {
return Integer.MAX_VALUE;
}

/**
* Human-readable name for this section, localized for the current user locale.
* Resolves the same heading message key the PDF rendering uses
* (patientdocuments.visitSummary.section.<key>.heading), falling back to a
* humanized form of getSectionKey() when no message is defined — so downstream
* sections get a usable label without shipping message bundles.
*/
default String getLabel() {
String key = PatientDocumentsConstants.MODULE_ARTIFACT_ID + "." + PatientDocumentsConstants.VISIT_SUMMARY_ID
+ ".section." + getSectionKey() + ".heading";
String fallback = humanizeSectionKey(getSectionKey());
try {
return Context.getMessageSourceService().getMessage(key, null, fallback, Context.getLocale());
}
catch (Exception e) {
return fallback;
}
}

/**
* Whether administrators may switch this section off via configuration.
* Sections that must appear on every PDF (facility identity, patient identity,
* audit footer) return false so config UIs can disable their toggle.
*/
default boolean isToggleable() {
return true;
}

/**
* Build this section's XML elements into the document.
* The Visit gives access to both the visit and its patient via visit.getPatient().
* Implementations should handle exceptions gracefully.
* TypedSection renders a section-error element on failure for clinical safety visibility.
*/
void renderXml(Document doc, Element root, Visit visit);

/**
* Splits a camelCase section key into capitalized words, e.g. "labResults" becomes
* "Lab Results". Used as the getLabel() fallback when no heading message exists.
*/
static String humanizeSectionKey(String sectionKey) {
if (sectionKey == null || sectionKey.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append(Character.toUpperCase(sectionKey.charAt(0)));
for (int i = 1; i < sectionKey.length(); i++) {
char c = sectionKey.charAt(i);
if (Character.isUpperCase(c)) {
sb.append(' ');
}
sb.append(c);
}
return sb.toString();
}
}
4 changes: 4 additions & 0 deletions api/src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ ${project.parent.artifactId}.visitSummary.section.labResults.flag.abnormal=Abnor
${project.parent.artifactId}.visitSummary.section.labResults.flag.critically_abnormal=Critically Abnormal
${project.parent.artifactId}.visitSummary.section.labResults.unknown=Unknown

# Visit summary - Facility Header section
${project.parent.artifactId}.visitSummary.section.facilityHeader.heading=Facility Header

# Visit summary - Footer section
${project.parent.artifactId}.visitSummary.section.footer.heading=Footer
${project.parent.artifactId}.visitSummary.section.footer.lbl.printedBy=Printed by:
${project.parent.artifactId}.visitSummary.section.footer.lbl.systemId=System ID:

Expand Down
4 changes: 4 additions & 0 deletions api/src/main/resources/messages_ar.properties
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ ${project.parent.artifactId}.visitSummary.section.labResults.flag.abnormal=Abnor
${project.parent.artifactId}.visitSummary.section.labResults.flag.critically_abnormal=Critically Abnormal
${project.parent.artifactId}.visitSummary.section.labResults.unknown=Unknown

# Visit summary - Facility Header section
${project.parent.artifactId}.visitSummary.section.facilityHeader.heading=Facility Header

# Visit summary - Footer section (English placeholders pending native-speaker translation)
${project.parent.artifactId}.visitSummary.section.footer.heading=Footer
${project.parent.artifactId}.visitSummary.section.footer.lbl.printedBy=Printed by:
${project.parent.artifactId}.visitSummary.section.footer.lbl.systemId=System ID:

Expand Down
4 changes: 4 additions & 0 deletions api/src/main/resources/messages_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ ${project.parent.artifactId}.visitSummary.section.labResults.flag.abnormal=Abnor
${project.parent.artifactId}.visitSummary.section.labResults.flag.critically_abnormal=Critically Abnormal
${project.parent.artifactId}.visitSummary.section.labResults.unknown=Unknown

# Visit summary - Facility Header section
${project.parent.artifactId}.visitSummary.section.facilityHeader.heading=Facility Header

# Visit summary - Footer section (English placeholders pending native-speaker translation)
${project.parent.artifactId}.visitSummary.section.footer.heading=Footer
${project.parent.artifactId}.visitSummary.section.footer.lbl.printedBy=Printed by:
${project.parent.artifactId}.visitSummary.section.footer.lbl.systemId=System ID:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.patientdocuments.api.section;

import java.util.Locale;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openmrs.Visit;
import org.openmrs.api.context.Context;
import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
* Tests for the VisitSummarySection default methods (getLabel, isToggleable),
* exercised through an anonymous implementation the way a downstream module
* would provide one — no changes in this module, no message bundle.
*/
public class VisitSummarySectionDefaultsTest extends BaseModuleContextSensitiveTest {

private VisitSummarySection sectionWithKey(String sectionKey) {
return new VisitSummarySection() {

@Override
public String getSectionKey() {
return sectionKey;
}

@Override
public boolean isEnabled() {
return true;
}

@Override
public void renderXml(Document doc, Element root, Visit visit) {
// no-op: these tests exercise getLabel()/isToggleable() only, never rendering
}
};
}

@BeforeEach
public void setUp() {
Context.setLocale(Locale.ENGLISH);
}

@Test
public void getLabel_shouldResolveHeadingMessageForKnownSectionKey() {
Assertions.assertEquals("Vital Signs", sectionWithKey("vitals").getLabel());
}

@Test
public void getLabel_shouldHumanizeSectionKeyWhenNoHeadingMessageExists() {
Assertions.assertEquals("Referral Notes", sectionWithKey("referralNotes").getLabel());
}

@Test
public void isToggleable_shouldDefaultToTrue() {
Assertions.assertTrue(sectionWithKey("referralNotes").isToggleable());
}

@Test
public void humanizeSectionKey_shouldHandleSingleWordAndEmptyKeys() {
Assertions.assertEquals("Vitals", VisitSummarySection.humanizeSectionKey("vitals"));
Assertions.assertEquals("", VisitSummarySection.humanizeSectionKey(""));
Assertions.assertEquals("", VisitSummarySection.humanizeSectionKey(null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.patientdocuments.web.rest.controller;

import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.MODULE_ARTIFACT_ID;
import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.VISIT_SUMMARY_ID;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

import org.openmrs.api.APIAuthenticationException;
import org.openmrs.api.context.Context;
import org.openmrs.api.context.ContextAuthenticationException;
import org.openmrs.module.patientdocuments.api.section.VisitSummarySection;
import org.openmrs.module.webservices.rest.SimpleObject;
import org.openmrs.module.webservices.rest.web.RestConstants;
import org.openmrs.module.webservices.rest.web.v1_0.controller.BaseRestController;
import org.openmrs.util.PrivilegeConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
* REST controller that lists the registered visit summary sections with their
* effective configuration, for admin/config UIs.
* <p>
* Accessible at: {@code GET /rest/v1/patientdocuments/visitSummary/sections}
* <p>
* Returns the standard REST collection shape — a {@code results} array of
* {@code {sectionKey, label, enabled, order, toggleable}} objects sorted by
* effective order, the same order the PDF renderer applies.
*/
@Controller
@RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/" + MODULE_ARTIFACT_ID + "/" + VISIT_SUMMARY_ID
+ "/sections")
public class VisitSummarySectionsController extends BaseRestController {

private static final Logger logger = LoggerFactory.getLogger(VisitSummarySectionsController.class);

@Autowired(required = false)
private List<VisitSummarySection> sections;

@RequestMapping(method = RequestMethod.GET)
public ResponseEntity<Object> getSections() {
try {
Context.requirePrivilege(PrivilegeConstants.GET_GLOBAL_PROPERTIES);

List<SimpleObject> results = new ArrayList<>();
if (sections != null) {
List<VisitSummarySection> ordered = new ArrayList<>(sections);
ordered.sort(Comparator.comparingInt(VisitSummarySection::getOrder));
for (VisitSummarySection section : ordered) {
SimpleObject entry = new SimpleObject();
entry.add("sectionKey", section.getSectionKey());
entry.add("label", section.getLabel());
entry.add("enabled", section.isEnabled());
entry.add("order", section.getOrder());
entry.add("toggleable", section.isToggleable());
results.add(entry);
}
}
return new ResponseEntity<>(new SimpleObject().add("results", results), HttpStatus.OK);
}
catch (APIAuthenticationException | ContextAuthenticationException e) {
logger.warn("Privilege check failed for visit summary sections request: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).contentType(MediaType.TEXT_PLAIN).body("Access denied");
}
catch (Exception e) {
logger.error("An error occurred while processing the request", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType(MediaType.TEXT_PLAIN)
.body("Error listing visit summary sections");
}
}
}
Loading