diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt new file mode 100644 index 0000000000..819d95eb85 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2024 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.smartregister.fhircore.engine.configuration + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +import org.smartregister.fhircore.engine.util.extension.interpolate + +@Serializable +@Parcelize +data class PdfConfig( + val title: String? = null, + val titleSuffix: String? = null, + val structureReference: String? = null, + val subjectReference: String? = null, + val questionnaireReferences: List = emptyList(), +) : java.io.Serializable, Parcelable { + + fun interpolate(computedValuesMap: Map) = + this.copy( + title = title?.interpolate(computedValuesMap), + titleSuffix = titleSuffix?.interpolate(computedValuesMap), + structureReference = structureReference?.interpolate(computedValuesMap), + subjectReference = subjectReference?.interpolate(computedValuesMap), + questionnaireReferences = questionnaireReferences.map { it.interpolate(computedValuesMap) }, + ) +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt index 60da3c1f83..1307c501e9 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt @@ -66,8 +66,6 @@ data class QuestionnaireConfig( val managingEntityRelationshipCode: String? = null, val uniqueIdAssignment: UniqueIdAssignmentConfig? = null, val linkIds: List? = null, - val htmlBinaryId: String? = null, - val htmlTitle: String? = null, ) : java.io.Serializable, Parcelable { fun interpolate(computedValuesMap: Map) = @@ -102,8 +100,6 @@ data class QuestionnaireConfig( uniqueIdAssignment?.copy(linkId = uniqueIdAssignment.linkId.interpolate(computedValuesMap)), linkIds = linkIds?.onEach { it.linkId.interpolate(computedValuesMap) }, saveButtonText = saveButtonText?.interpolate(computedValuesMap), - htmlBinaryId = htmlBinaryId?.interpolate(computedValuesMap), - htmlTitle = htmlTitle?.interpolate(computedValuesMap), ) } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ActionConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ActionConfig.kt index 0bd988d40b..3b9f5b3acd 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ActionConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/domain/model/ActionConfig.kt @@ -22,6 +22,7 @@ import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.ResourceType +import org.smartregister.fhircore.engine.configuration.PdfConfig import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.profile.ManagingEntityConfig import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger @@ -42,6 +43,7 @@ data class ActionConfig( val toolBarHomeNavigation: ToolBarHomeNavigation = ToolBarHomeNavigation.OPEN_DRAWER, val popNavigationBackStack: Boolean? = null, val multiSelectViewConfig: MultiSelectViewConfig? = null, + val pdfConfig: PdfConfig? = null, ) : Parcelable, java.io.Serializable { fun paramsBundle(computedValuesMap: Map = emptyMap()): Bundle = Bundle().apply { diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt index 28aaf581ed..4110986616 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/pdf/HtmlPopulator.kt @@ -16,32 +16,55 @@ package org.smartregister.fhircore.engine.pdf +import java.util.Date import java.util.regex.Matcher import java.util.regex.Pattern import org.hl7.fhir.r4.model.BaseDateTimeType import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent import org.smartregister.fhircore.engine.util.extension.allItems +import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid import org.smartregister.fhircore.engine.util.extension.formatDate import org.smartregister.fhircore.engine.util.extension.makeItReadable import org.smartregister.fhircore.engine.util.extension.valueToString /** * HtmlPopulator class is responsible for processing an HTML template by replacing custom tags with - * data from a QuestionnaireResponse. The class uses various regex patterns to find and replace - * custom tags such as @is-not-empty, @answer-as-list, @answer, @submitted-date, and @contains. + * data from QuestionnaireResponses. The class uses various regex patterns to find and replace + * custom tags such as @is-not-empty, @answer-as-list, @answer, @submitted-date, @contains, + * and @is-questionnaire-submitted. * - * @property questionnaireResponse The QuestionnaireResponse object containing data for replacement. + * @property questionnaireResponses The QuestionnaireResponses object containing data for + * replacement. */ class HtmlPopulator( - private val questionnaireResponse: QuestionnaireResponse, + questionnaireResponses: List, ) { + private var answerMap: Map> + private var submittedDateMap: Map + private var questionnaireIds: List - // Map to store questionnaire response items keyed by their linkId - private val questionnaireResponseItemMap = - questionnaireResponse.allItems.associateBy( - keySelector = { it.linkId }, - valueTransform = { it.answer }, - ) + init { + val answerMap = mutableMapOf>() + val submittedDateMap = mutableMapOf() + val questionnaireIds = mutableListOf() + + questionnaireResponses.forEach { questionnaireResponse -> + val questionnaireId = questionnaireResponse.questionnaire.extractLogicalIdUuid() + questionnaireResponse.allItems + .associateBy( + keySelector = { "$questionnaireId/${it.linkId}" }, + valueTransform = { it.answer }, + ) + .let { answerMap.putAll(it) } + submittedDateMap[questionnaireId] = questionnaireResponse.meta.lastUpdated ?: Date() + questionnaireIds.add(questionnaireId) + } + + this.answerMap = answerMap + this.submittedDateMap = submittedDateMap + this.questionnaireIds = questionnaireIds + } /** * Populates the provided HTML template with data from the QuestionnaireResponse. @@ -77,6 +100,10 @@ class HtmlPopulator( val matcher = containsPattern.matcher(html.substring(i)) if (matcher.find()) processContains(i, html, matcher) else i++ } + html.startsWith("@is-questionnaire-submitted", i) -> { + val matcher = isQuestionnaireSubmittedPattern.matcher(html.substring(i)) + if (matcher.find()) processIsQuestionnaireSubmitted(i, html, matcher) else i++ + } else -> i++ } } @@ -94,7 +121,7 @@ class HtmlPopulator( private fun processIsNotEmpty(i: Int, html: StringBuilder, matcher: Matcher) { val linkId = matcher.group(1) val content = matcher.group(2) ?: "" - val doesAnswerExist = questionnaireResponseItemMap.getOrDefault(linkId, listOf()).isNotEmpty() + val doesAnswerExist = answerMap.getOrDefault(linkId, listOf()).isNotEmpty() if (doesAnswerExist) { html.replace(i, matcher.end() + i, content) // Start index is the index of '@' symbol, End index is the index after the ')' symbol. @@ -119,8 +146,7 @@ class HtmlPopulator( private fun processAnswerAsList(i: Int, html: StringBuilder, matcher: Matcher) { val linkId = matcher.group(1) val answerAsList = - questionnaireResponseItemMap.getOrDefault(linkId, listOf()).joinToString(separator = "") { - answer -> + answerMap.getOrDefault(linkId, listOf()).joinToString(separator = "") { answer -> "
  • ${answer.value.valueToString()}
  • " } html.replace(i, matcher.end() + i, answerAsList) @@ -137,7 +163,7 @@ class HtmlPopulator( val linkId = matcher.group(1) val dateFormat = matcher.group(2) val answer = - questionnaireResponseItemMap.getOrDefault(linkId, listOf()).joinToString { answer -> + answerMap.getOrDefault(linkId, listOf()).joinToString { answer -> if (dateFormat == null) { answer.value.valueToString() } else answer.value.valueToString(dateFormat) @@ -153,12 +179,13 @@ class HtmlPopulator( * @param matcher The Matcher object for the regex pattern. */ private fun processSubmittedDate(i: Int, html: StringBuilder, matcher: Matcher) { - val dateFormat = matcher.group(1) + val questionnaireId = matcher.group(1) + val dateFormat = matcher.group(2) val date = if (dateFormat == null) { - questionnaireResponse.meta.lastUpdated.formatDate() + submittedDateMap.getOrDefault(questionnaireId, Date()).formatDate() } else { - questionnaireResponse.meta.lastUpdated.formatDate(dateFormat) + submittedDateMap.getOrDefault(questionnaireId, Date()).formatDate(dateFormat) } html.replace(i, matcher.end() + i, date) } @@ -176,7 +203,7 @@ class HtmlPopulator( val indicator = matcher.group(2) ?: "" val content = matcher.group(3) ?: "" val doesAnswerExist = - questionnaireResponseItemMap.getOrDefault(linkId, listOf()).any { + answerMap.getOrDefault(linkId, listOf()).any { when { it.hasValueCoding() -> it.valueCoding.code == indicator it.hasValueStringType() -> it.valueStringType.value.contains(indicator) @@ -197,14 +224,39 @@ class HtmlPopulator( } } + /** + * Processes the @is-questionnaire-submitted tag by checking if the corresponding + * [QuestionnaireResponse] exists. Replaces the tag with the content if the indicator is true, + * otherwise removes the tag. + * + * @param i The starting index of the tag in the HTML. + * @param html The StringBuilder containing the HTML. + * @param matcher The Matcher object for the regex pattern. + */ + private fun processIsQuestionnaireSubmitted(i: Int, html: StringBuilder, matcher: Matcher) { + val id = matcher.group(1) + val content = matcher.group(2) ?: "" + val doesQuestionnaireExists = questionnaireIds.contains(id) + if (doesQuestionnaireExists) { + html.replace(i, matcher.end() + i, content) + } else { + html.replace(i, matcher.end() + i, "") + } + } + companion object { // Compile regex patterns for different tags private val isNotEmptyPattern = Pattern.compile("@is-not-empty\\('([^']+)'\\)((?s).*?)@is-not-empty\\('\\1'\\)") private val answerAsListPattern = Pattern.compile("@answer-as-list\\('([^']+)'\\)") private val answerPattern = Pattern.compile("@answer\\('([^']+)'(?:,'([^']+)')?\\)") - private val submittedDatePattern = Pattern.compile("@submitted-date(?:\\('([^']+)'\\))?") + private val submittedDatePattern = + Pattern.compile("@submitted-date\\('([^']+)'(?:,'([^']+)')?\\)") private val containsPattern = Pattern.compile("@contains\\('([^']+)','([^']+)'\\)((?s).*?)@contains\\('\\1'\\)") + private val isQuestionnaireSubmittedPattern = + Pattern.compile( + "@is-questionnaire-submitted\\('([^']+)'\\)((?s).*?)@is-questionnaire-submitted\\('\\1'\\)", + ) } } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/pdf/HtmlPopulatorTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/pdf/HtmlPopulatorTest.kt index 2a348b4376..df51ac3af6 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/pdf/HtmlPopulatorTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/pdf/HtmlPopulatorTest.kt @@ -35,223 +35,311 @@ class HtmlPopulatorTest { @Test fun testIsNotEmptyShouldShowContentWhenAnswerExistInQR() { - val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 1", "code 1", "display 1") - }, - ) + val html = "@is-not-empty('1234/link-a')

    Text

    @is-not-empty('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

    Text

    ", populatedHtml) } @Test fun testIsNotEmptyShouldHideContentWhenAnswerIsEmptyInQR() { - val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = emptyList() - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "@is-not-empty('1234/link-a')

    Text

    @is-not-empty('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = emptyList() + } + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("", populatedHtml) } @Test fun testIsNotEmptyShouldHideContentWhenAnswerNotExistInQR() { - val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { addItem().apply { linkId = "link-a" } } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "@is-not-empty('1234/link-a')

    Text

    @is-not-empty('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { linkId = "link-a" } + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("", populatedHtml) } @Test fun testIsNotEmptyShouldHideContentWhenLinkIdNotExistInQR() { - val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-a')" - val questionnaireResponse = QuestionnaireResponse() - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "@is-not-empty('1234/link-a')

    Text

    @is-not-empty('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("", populatedHtml) } @Test fun testIsNotEmptyShouldShowMalformedTagAndContentIfLinkIdOfBothTagDoesNotMatch() { - val html = "@is-not-empty('link-a')

    Text

    @is-not-empty('link-b')" - val questionnaireResponse = QuestionnaireResponse() - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "@is-not-empty('1234/link-a')

    Text

    @is-not-empty('1234/link-b')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) - Assert.assertEquals("@is-not-empty('link-a')

    Text

    @is-not-empty('link-b')", populatedHtml) + Assert.assertEquals( + "@is-not-empty('1234/link-a')

    Text

    @is-not-empty('1234/link-b')", + populatedHtml, + ) } @Test fun testIsNotEmptyShouldShowMalformedTagAndContentIfOnly1TagExist() { - val html = "@is-not-empty('link-a')

    Text

    " - val questionnaireResponse = QuestionnaireResponse() - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "@is-not-empty('1234/link-a')

    Text

    " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) - Assert.assertEquals("@is-not-empty('link-a')

    Text

    ", populatedHtml) + Assert.assertEquals("@is-not-empty('1234/link-a')

    Text

    ", populatedHtml) } @Test fun testIsNotEmptyShouldShowContentAndNestedMalformedTagIfAnswerOfRootTagExist() { - val html = "@is-not-empty('link-a')@is-not-empty('link-b')

    Text

    @is-not-empty('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 1", "code 1", "display 1") - }, - ) + val html = + "@is-not-empty('1234/link-a')@is-not-empty('1234/link-b')

    Text

    @is-not-empty('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) - Assert.assertEquals("@is-not-empty('link-b')

    Text

    ", populatedHtml) + Assert.assertEquals("@is-not-empty('1234/link-b')

    Text

    ", populatedHtml) } @Test fun testIsNotEmptyShouldHideContentAndNestedMalformedTagIfAnswerOfRootTagIsNotExist() { - val html = "@is-not-empty('link-a')@is-not-empty('link-b')

    Text

    @is-not-empty('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { addItem().apply { linkId = "link-a" } } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = + "@is-not-empty('1234/link-a')@is-not-empty('1234/link-b')

    Text

    @is-not-empty('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { linkId = "link-a" } + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("", populatedHtml) } @Test fun testIsNotEmptyShouldHideContentAndNestedMalformedTagIfAnswerOfRootTagIsEmpty() { - val html = "@is-not-empty('link-a')@is-not-empty('link-b')

    Text

    @is-not-empty('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = emptyList() - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = + "@is-not-empty('1234/link-a')@is-not-empty('1234/link-b')

    Text

    @is-not-empty('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = emptyList() + } + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("", populatedHtml) } @Test fun testIsNotEmptyShouldShowEmptyContentIfAnswerExist() { - val html = "@is-not-empty('link-a')@is-not-empty('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 1", "code 1", "display 1") - }, - ) + val html = "@is-not-empty('1234/link-a')@is-not-empty('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("", populatedHtml) } @Test fun testProcessAnswerAsListShouldShowAnswerAsListWhenAnswerExistInQR() { - val html = "
      @answer-as-list('link-a')
    " - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 1", "code 1", "display 1") - }, - ) - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 2", "code 2", "display 2") - }, - ) + val html = "
      @answer-as-list('1234/link-a')
    " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 2", "code 2", "display 2") + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("
    • display 1
    • display 2
    ", populatedHtml) } @Test fun testProcessAnswerAsListShouldShowEmptyAnswerAsListWhenAnswerNotExistInQR() { - val html = "
      @answer-as-list('link-a')
    " - val questionnaireResponse = - QuestionnaireResponse().apply { addItem().apply { linkId = "link-a" } } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "
      @answer-as-list('1234/link-a')
    " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { linkId = "link-a" } + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("
      ", populatedHtml) } @Test fun testProcessAnswerAsListShouldShowEmptyAnswerAsListWhenLinkIdNotExistInQR() { - val html = "
        @answer-as-list('link-a')
      " - val questionnaireResponse = QuestionnaireResponse() - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "
        @answer-as-list('1234/link-a')
      " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("
        ", populatedHtml) } @Test fun testProcessAnswerShouldShowAnswerWhenAnswerExistInQR() { - val html = "

        @answer('link-a')

        " - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { value = StringType("string 1") }, - ) + val html = "

        @answer('1234/link-a')

        " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = StringType("string 1") }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        string 1

        ", populatedHtml) } @Test fun testProcessAnswerShouldShowEmptyAnswerWhenAnswerNotExistInQR() { - val html = "

        @answer('link-a')

        " - val questionnaireResponse = - QuestionnaireResponse().apply { addItem().apply { linkId = "link-a" } } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "

        @answer('1234/link-a')

        " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { linkId = "link-a" } + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        ", populatedHtml) } @Test fun testProcessAnswerShouldShowEmptyAnswerWhenLinkIdNotExistInQR() { - val html = "

        @answer('link-a')

        " - val questionnaireResponse = QuestionnaireResponse() - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "

        @answer('1234/link-a')

        " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        ", populatedHtml) } @@ -260,21 +348,25 @@ class HtmlPopulatorTest { fun testProcessAnswerShouldShowDateAnswerWhenAnswerOfTypeDateExistInQR() { val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } val specificDate: Date = calendar.time - val html = "

        @answer('link-a')

        " - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = DateTimeType(specificDate) - }, - ) + val html = "

        @answer('1234/link-a')

        " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = DateTimeType(specificDate) + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        14-May-2024

        ", populatedHtml) } @@ -283,21 +375,25 @@ class HtmlPopulatorTest { fun testProcessAnswerShouldShowDateAnswerWithFormatWhenDateFormatExistInTheTag() { val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } val specificDate: Date = calendar.time - val html = "

        @answer('link-a','MMMM d, yyyy')

        " - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = DateTimeType(specificDate) - }, - ) + val html = "

        @answer('1234/link-a','MMMM d, yyyy')

        " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = DateTimeType(specificDate) + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        May 14, 2024

        ", populatedHtml) } @@ -306,10 +402,16 @@ class HtmlPopulatorTest { fun testProcessSubmittedDateShouldShow() { val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } val specificDate: Date = calendar.time - val html = "

        @submitted-date

        " - val questionnaireResponse = - QuestionnaireResponse().apply { meta = Meta().apply { lastUpdated = specificDate } } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "

        @submitted-date('1234')

        " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + meta = Meta().apply { lastUpdated = specificDate } + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        14-May-2024

        ", populatedHtml) } @@ -318,183 +420,249 @@ class HtmlPopulatorTest { fun testProcessSubmittedDateShouldShowWithFormatWhenDateFormatExistInTheTag() { val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } val specificDate: Date = calendar.time - val html = "

        @submitted-date('MMMM d, yyyy')

        " - val questionnaireResponse = - QuestionnaireResponse().apply { meta = Meta().apply { lastUpdated = specificDate } } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + val html = "

        @submitted-date('1234','MMMM d, yyyy')

        " + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + meta = Meta().apply { lastUpdated = specificDate } + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        May 14, 2024

        ", populatedHtml) } @Test fun testProcessContainsShouldShowContentWhenIndicatorCodeMatchesWithAnswerOfTypeCoding() { - val html = "@contains('link-a','code 2')

        Text

        @contains('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 1", "code 1", "display 1") - }, - ) - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 2", "code 2", "display 2") - }, - ) + val html = "@contains('1234/link-a','code 2')

        Text

        @contains('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 2", "code 2", "display 2") + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        Text

        ", populatedHtml) } @Test fun testProcessContainsShouldHideContentWhenIndicatorCodeDoesNotMatchWithAnswerOfTypeCoding() { - val html = "@contains('link-a','code 3')

        Text

        @contains('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 1", "code 1", "display 1") - }, - ) - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Coding("system 2", "code 2", "display 2") - }, - ) + val html = "@contains('1234/link-a','code 3')

        Text

        @contains('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 1", "code 1", "display 1") + }, + ) + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Coding("system 2", "code 2", "display 2") + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("", populatedHtml) } @Test fun testProcessContainsShouldShowContentWhenIndicatorStringIsContainedInAnswerOfTypeString() { - val html = "@contains('link-a','basket')

        Text

        @contains('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { value = StringType("basketball") }, - ) + val html = "@contains('1234/link-a','basket')

        Text

        @contains('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = StringType("basketball") + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        Text

        ", populatedHtml) } @Test fun testProcessContainsShouldShowContentWhenIndicatorIntegerMatchesAnswerOfTypeInteger() { - val html = "@contains('link-a','10')

        Text

        @contains('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { value = IntegerType("10") }, - ) + val html = "@contains('1234/link-a','10')

        Text

        @contains('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = IntegerType("10") }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        Text

        ", populatedHtml) } @Test fun testProcessContainsShouldShowContentWhenIndicatorDecimalMatchesAnswerOfTypeDecimal() { - val html = "@contains('link-a','1.5')

        Text

        @contains('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { value = DecimalType("1.5") }, - ) + val html = "@contains('1234/link-a','1.5')

        Text

        @contains('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = DecimalType("1.5") }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        Text

        ", populatedHtml) } @Test fun testProcessContainsShouldShowContentWhenIndicatorBooleanMatchesAnswerOfTypeBoolean() { - val html = "@contains('link-a','true')

        Text

        @contains('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { value = BooleanType("true") }, - ) + val html = "@contains('1234/link-a','true')

        Text

        @contains('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { value = BooleanType("true") }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        Text

        ", populatedHtml) } @Test fun testProcessContainsShouldShowContentWhenIndicatorQuantityMatchesAnswerOfTypeQuantity() { - val html = "@contains('link-a','3 years')

        Text

        @contains('link-a')" - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = Quantity(null, 3, "system", "years", "years") - }, - ) + val html = "@contains('1234/link-a','3 years')

        Text

        @contains('1234/link-a')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = Quantity(null, 3, "system", "years", "years") + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        Text

        ", populatedHtml) } @Test fun testProcessContainsShouldShowContentWhenIndicatorDateMatchesAnswerOfTypeDate() { - val html = "@contains('link-a','14-May-2024')

        Text

        @contains('link-a')" + val html = "@contains('1234/link-a','14-May-2024')

        Text

        @contains('1234/link-a')" val calendar = Calendar.getInstance().apply { set(2024, Calendar.MAY, 14) } val specificDate: Date = calendar.time - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem().apply { - linkId = "link-a" - answer = buildList { - add( - QuestionnaireResponseItemAnswerComponent().apply { - value = DateTimeType(specificDate) - }, - ) + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/1234" + addItem().apply { + linkId = "link-a" + answer = buildList { + add( + QuestionnaireResponseItemAnswerComponent().apply { + value = DateTimeType(specificDate) + }, + ) + } } - } - } - val htmlPopulator = HtmlPopulator(questionnaireResponse) + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("

        Text

        ", populatedHtml) + } + + @Test + fun testProcessIsQuestionnaireSubmittedShouldShowContentWhenTheRelatedQuestionnaireResponseExists() { + val html = + "@is-questionnaire-submitted('q-1234')

        Text

        @is-questionnaire-submitted('q-1234')" + val questionnaireResponses = + listOf( + QuestionnaireResponse().apply { + id = "1234" + questionnaire = "Questionnaire/q-1234" + }, + ) + val htmlPopulator = HtmlPopulator(questionnaireResponses) val populatedHtml = htmlPopulator.populateHtml(html) Assert.assertEquals("

        Text

        ", populatedHtml) } + + @Test + fun testProcessIsQuestionnaireSubmittedShouldNotShowContentWhenTheRelatedQuestionnaireResponseDoesNotExists() { + val html = + "@is-questionnaire-submitted('q-1234')

        Text

        @is-questionnaire-submitted('q-1234')" + val questionnaireResponses = listOf() + val htmlPopulator = HtmlPopulator(questionnaireResponses) + val populatedHtml = htmlPopulator.populateHtml(html) + Assert.assertEquals("", populatedHtml) + } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherFragment.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherFragment.kt index e20d72e111..6d9d0376b5 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherFragment.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherFragment.kt @@ -30,7 +30,7 @@ import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.QuestionnaireResponse import org.jetbrains.annotations.VisibleForTesting import org.smartregister.fhircore.engine.R -import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig +import org.smartregister.fhircore.engine.configuration.PdfConfig import org.smartregister.fhircore.engine.pdf.HtmlPopulator import org.smartregister.fhircore.engine.util.extension.decodeJson import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid @@ -38,8 +38,8 @@ import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid /** * A fragment for generating and displaying a PDF based on a questionnaire response. * - * This fragment uses the provided [QuestionnaireConfig] to retrieve a questionnaire response, - * populate an HTML template with the response data, and generate a PDF. + * This fragment uses the provided [PdfConfig] to retrieve a questionnaire response, populate an + * HTML template with the response data, and generate a PDF. */ @AndroidEntryPoint class PdfLauncherFragment : DialogFragment() { @@ -52,35 +52,40 @@ class PdfLauncherFragment : DialogFragment() { super.onCreate(savedInstanceState) if (!this::pdfGenerator.isInitialized) pdfGenerator = PdfGenerator(requireContext()) - val questionnaireConfig = getQuestionnaireConfig() + val pdfConfig = getPdfConfig() - val questionnaireId = questionnaireConfig.id.extractLogicalIdUuid() - val subjectId = questionnaireConfig.resourceIdentifier!!.extractLogicalIdUuid() - val subjectType = questionnaireConfig.resourceType!! - val htmlBinaryId = questionnaireConfig.htmlBinaryId!!.extractLogicalIdUuid() - val htmlTitle = questionnaireConfig.htmlTitle ?: getString(R.string.default_html_title) + val structureId = pdfConfig.structureReference!!.extractLogicalIdUuid() + val title = StringBuilder().append(pdfConfig.title ?: getString(R.string.default_html_title)) + val titleSuffix = pdfConfig.titleSuffix + val subjectReference = pdfConfig.subjectReference!! + val questionnaireIds = + pdfConfig.questionnaireReferences.map { it.extractLogicalIdUuid() } ?: emptyList() lifecycleScope.launch(Dispatchers.IO) { - val questionnaireResponse = - pdfLauncherViewModel.retrieveQuestionnaireResponse( - questionnaireId, - subjectId, - subjectType, - ) - val htmlBinary = pdfLauncherViewModel.retrieveBinary(htmlBinaryId) - generatePdf(questionnaireResponse, htmlBinary, htmlTitle) + val questionnaireResponses = + questionnaireIds.mapNotNull { questionnaireId -> + pdfLauncherViewModel.retrieveQuestionnaireResponse( + questionnaireId, + subjectReference, + ) + } + val htmlBinary = pdfLauncherViewModel.retrieveBinary(structureId) + + if (titleSuffix != null) title.append(" - $titleSuffix") + + generatePdf(questionnaireResponses, htmlBinary, title.toString()) } } /** * Retrieves and decodes the questionnaire configuration from the fragment arguments. * - * @return the decoded [QuestionnaireConfig] object. + * @return the decoded [PdfConfig] object. * @throws IllegalArgumentException if the questionnaire config is not found in arguments. */ - private fun getQuestionnaireConfig(): QuestionnaireConfig { + private fun getPdfConfig(): PdfConfig { val jsonConfig = - requireArguments().getString(EXTRA_QUESTIONNAIRE_CONFIG_KEY) + requireArguments().getString(EXTRA_PDF_CONFIG_KEY) ?: throw IllegalArgumentException("Questionnaire config not found in arguments") return jsonConfig.decodeJson() } @@ -88,22 +93,22 @@ class PdfLauncherFragment : DialogFragment() { /** * Generates a PDF using the provided questionnaire response and HTML template. * - * @param questionnaireResponse the [QuestionnaireResponse] object containing user responses. + * @param questionnaireResponses containing user responses. * @param htmlBinary the [Binary] object containing the HTML template. * @param htmlTitle the title to be used for the generated PDF. */ private suspend fun generatePdf( - questionnaireResponse: QuestionnaireResponse?, + questionnaireResponses: List, htmlBinary: Binary?, htmlTitle: String, ) { - if (questionnaireResponse == null || htmlBinary == null) { + if (questionnaireResponses.isEmpty() || htmlBinary == null) { dismiss() return } val htmlContent = htmlBinary.content.decodeToString() - val populatedHtml = HtmlPopulator(questionnaireResponse).populateHtml(htmlContent) + val populatedHtml = HtmlPopulator(questionnaireResponses).populateHtml(htmlContent) withContext(Dispatchers.Main) { pdfGenerator.generatePdfWithHtml(populatedHtml, htmlTitle) { dismiss() } @@ -123,10 +128,10 @@ class PdfLauncherFragment : DialogFragment() { */ fun launch(appCompatActivity: AppCompatActivity, questionnaireConfigJson: String) { PdfLauncherFragment() - .apply { arguments = bundleOf(EXTRA_QUESTIONNAIRE_CONFIG_KEY to questionnaireConfigJson) } + .apply { arguments = bundleOf(EXTRA_PDF_CONFIG_KEY to questionnaireConfigJson) } .show(appCompatActivity.supportFragmentManager, PdfLauncherFragment::class.java.simpleName) } - @VisibleForTesting const val EXTRA_QUESTIONNAIRE_CONFIG_KEY = "questionnaire_config" + @VisibleForTesting const val EXTRA_PDF_CONFIG_KEY = "pdf_config" } } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherViewModel.kt index fa50c05d44..b8426a3f7f 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherViewModel.kt @@ -44,14 +44,12 @@ constructor( * Retrieve the [QuestionnaireResponse] for the given questionnaire and subject. * * @param questionnaireId The ID of the questionnaire. - * @param subjectId The ID of the subject. - * @param subjectType The type of the subject (resource type). + * @param subjectReference The ID of the subject. * @return The [QuestionnaireResponse] if found, otherwise null. */ suspend fun retrieveQuestionnaireResponse( questionnaireId: String, - subjectId: String, - subjectType: ResourceType, + subjectReference: String, ): QuestionnaireResponse? { val searchQuery = createQuestionnaireResponseSearchQuery(questionnaireId, subjectId, subjectType) @@ -64,17 +62,15 @@ constructor( * Create a search query for [QuestionnaireResponse]. * * @param questionnaireId The ID of the questionnaire. - * @param subjectId The ID of the subject. - * @param subjectType The type of the subject (resource type). + * @param subjectReference The ID of the subject. * @return The search query for [QuestionnaireResponse]. */ private fun createQuestionnaireResponseSearchQuery( questionnaireId: String, - subjectId: String, - subjectType: ResourceType, + subjectReference: String, ): Search { return Search(ResourceType.QuestionnaireResponse).apply { - filter(QuestionnaireResponse.SUBJECT, { value = "$subjectType/$subjectId" }) + filter(QuestionnaireResponse.SUBJECT, { value = subjectReference }) filter( QuestionnaireResponse.QUESTIONNAIRE, { value = "${ResourceType.Questionnaire}/$questionnaireId" }, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt index 713500d2ac..bf0e893a23 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/util/extensions/ConfigExtensions.kt @@ -225,13 +225,10 @@ fun ActionConfig.handleClickEvent( navController.navigate(MainNavigationScreen.LocationSelector.route, args) } ApplicationWorkflow.LAUNCH_PDF_GENERATION -> { - val questionnaireConfig = actionConfig.questionnaire ?: return - val questionnaireConfigInterpolated = questionnaireConfig.interpolate(computedValuesMap) + val pdfConfig = actionConfig.pdfConfig ?: return + val interpolatedPdfConfig = pdfConfig.interpolate(computedValuesMap) val appCompatActivity = (navController.context as AppCompatActivity) - PdfLauncherFragment.launch( - appCompatActivity, - questionnaireConfigInterpolated.encodeJson(), - ) + PdfLauncherFragment.launch(appCompatActivity, interpolatedPdfConfig.encodeJson()) } else -> return } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherFragmentTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherFragmentTest.kt index d0b23bb9c6..7f797681e4 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherFragmentTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/pdf/PdfLauncherFragmentTest.kt @@ -27,12 +27,11 @@ import io.mockk.verify import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.QuestionnaireResponse -import org.hl7.fhir.r4.model.ResourceType import org.junit.Before import org.junit.Rule import org.junit.Test import org.robolectric.Robolectric -import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig +import org.smartregister.fhircore.engine.configuration.PdfConfig import org.smartregister.fhircore.engine.util.extension.encodeJson import org.smartregister.fhircore.quest.app.fakes.HiltTestActivity import org.smartregister.fhircore.quest.robolectric.RobolectricTest @@ -53,27 +52,25 @@ class PdfLauncherFragmentTest : RobolectricTest() { @Test fun testPdfGeneration() = runBlocking { - val questionnaireResponse = QuestionnaireResponse() + val questionnaireResponse = QuestionnaireResponse().apply { questionnaire = "Questionnaire/id" } val htmlBinary = Binary().apply { content = "mock content".toByteArray() } - coEvery { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any(), any()) } returns + coEvery { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any()) } returns questionnaireResponse coEvery { pdfLauncherViewModel.retrieveBinary(any()) } returns htmlBinary - val questionnaireConfig = - QuestionnaireConfig( - id = "1", - resourceIdentifier = "123", - resourceType = ResourceType.Patient, - htmlBinaryId = "1234", - htmlTitle = "Title", + val pdfConfig = + PdfConfig( + title = "title", + titleSuffix = "suffix", + structureReference = "Binary/id", + subjectReference = "Patient/id", + questionnaireReferences = listOf("QuestionnaireResponse/id"), ) .encodeJson() val fragmentArgs = - Bundle().apply { - putString(PdfLauncherFragment.EXTRA_QUESTIONNAIRE_CONFIG_KEY, questionnaireConfig) - } + Bundle().apply { putString(PdfLauncherFragment.EXTRA_PDF_CONFIG_KEY, pdfConfig) } val activity = Robolectric.buildActivity(HiltTestActivity::class.java).create().resume().get() @@ -85,7 +82,7 @@ class PdfLauncherFragmentTest : RobolectricTest() { activity.supportFragmentManager.beginTransaction().add(fragment, null).commitNow() - coVerify { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any(), any()) } + coVerify { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any()) } coVerify { pdfLauncherViewModel.retrieveBinary(any()) } verify { pdfGenerator.generatePdfWithHtml(any(), any(), any()) } } @@ -95,24 +92,22 @@ class PdfLauncherFragmentTest : RobolectricTest() { val questionnaireResponse: QuestionnaireResponse? = null val htmlBinary = Binary().apply { content = "mock content".toByteArray() } - coEvery { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any(), any()) } returns + coEvery { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any()) } returns questionnaireResponse coEvery { pdfLauncherViewModel.retrieveBinary(any()) } returns htmlBinary - val questionnaireConfig = - QuestionnaireConfig( - id = "1", - resourceIdentifier = "123", - resourceType = ResourceType.Patient, - htmlBinaryId = "1234", - htmlTitle = "Title", + val pdfConfig = + PdfConfig( + title = "title", + titleSuffix = "suffix", + structureReference = "Binary/id", + subjectReference = "Patient/id", + questionnaireReferences = listOf("QuestionnaireResponse/id"), ) .encodeJson() val fragmentArgs = - Bundle().apply { - putString(PdfLauncherFragment.EXTRA_QUESTIONNAIRE_CONFIG_KEY, questionnaireConfig) - } + Bundle().apply { putString(PdfLauncherFragment.EXTRA_PDF_CONFIG_KEY, pdfConfig) } val activity = Robolectric.buildActivity(HiltTestActivity::class.java).create().resume().get() @@ -124,7 +119,7 @@ class PdfLauncherFragmentTest : RobolectricTest() { activity.supportFragmentManager.beginTransaction().add(fragment, null).commitNow() - coVerify { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any(), any()) } + coVerify { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any()) } coVerify { pdfLauncherViewModel.retrieveBinary(any()) } verify(inverse = true) { pdfGenerator.generatePdfWithHtml(any(), any(), any()) } } @@ -134,24 +129,22 @@ class PdfLauncherFragmentTest : RobolectricTest() { val questionnaireResponse = QuestionnaireResponse() val htmlBinary: Binary? = null - coEvery { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any(), any()) } returns + coEvery { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any()) } returns questionnaireResponse coEvery { pdfLauncherViewModel.retrieveBinary(any()) } returns htmlBinary - val questionnaireConfig = - QuestionnaireConfig( - id = "1", - resourceIdentifier = "123", - resourceType = ResourceType.Patient, - htmlBinaryId = "1234", - htmlTitle = "Title", + val pdfConfig = + PdfConfig( + title = "title", + titleSuffix = "suffix", + structureReference = "Binary/id", + subjectReference = "Patient/id", + questionnaireReferences = listOf("QuestionnaireResponse/id"), ) .encodeJson() val fragmentArgs = - Bundle().apply { - putString(PdfLauncherFragment.EXTRA_QUESTIONNAIRE_CONFIG_KEY, questionnaireConfig) - } + Bundle().apply { putString(PdfLauncherFragment.EXTRA_PDF_CONFIG_KEY, pdfConfig) } val activity = Robolectric.buildActivity(HiltTestActivity::class.java).create().resume().get() @@ -163,7 +156,7 @@ class PdfLauncherFragmentTest : RobolectricTest() { activity.supportFragmentManager.beginTransaction().add(fragment, null).commitNow() - coVerify { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any(), any()) } + coVerify { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any()) } coVerify { pdfLauncherViewModel.retrieveBinary(any()) } verify(inverse = true) { pdfGenerator.generatePdfWithHtml(any(), any(), any()) } } @@ -173,24 +166,22 @@ class PdfLauncherFragmentTest : RobolectricTest() { val questionnaireResponse: QuestionnaireResponse? = null val htmlBinary: Binary? = null - coEvery { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any(), any()) } returns + coEvery { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any()) } returns questionnaireResponse coEvery { pdfLauncherViewModel.retrieveBinary(any()) } returns htmlBinary - val questionnaireConfig = - QuestionnaireConfig( - id = "1", - resourceIdentifier = "123", - resourceType = ResourceType.Patient, - htmlBinaryId = "1234", - htmlTitle = "Title", + val pdfConfig = + PdfConfig( + title = "title", + titleSuffix = "suffix", + structureReference = "Binary/id", + subjectReference = "Patient/id", + questionnaireReferences = listOf("QuestionnaireResponse/id"), ) .encodeJson() val fragmentArgs = - Bundle().apply { - putString(PdfLauncherFragment.EXTRA_QUESTIONNAIRE_CONFIG_KEY, questionnaireConfig) - } + Bundle().apply { putString(PdfLauncherFragment.EXTRA_PDF_CONFIG_KEY, pdfConfig) } val activity = Robolectric.buildActivity(HiltTestActivity::class.java).create().resume().get() @@ -202,7 +193,7 @@ class PdfLauncherFragmentTest : RobolectricTest() { activity.supportFragmentManager.beginTransaction().add(fragment, null).commitNow() - coVerify { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any(), any()) } + coVerify { pdfLauncherViewModel.retrieveQuestionnaireResponse(any(), any()) } coVerify { pdfLauncherViewModel.retrieveBinary(any()) } verify(inverse = true) { pdfGenerator.generatePdfWithHtml(any(), any(), any()) } } diff --git a/docs/engineering/app/configuring/config-types/pdf.mdx b/docs/engineering/app/configuring/config-types/pdf.mdx new file mode 100644 index 0000000000..736e66df9e --- /dev/null +++ b/docs/engineering/app/configuring/config-types/pdf.mdx @@ -0,0 +1,42 @@ +--- +title: PDF +--- + +# PDF configuration + +Launch a PDF preview from a defined HTML structure and Questionnaire Responses that contains data to be populated into the PDF. App user then can save that previewed PDF into their device as a file. + +:::info +The [PDF Generation](https://docs.opensrp.io/engineering/app/configuring/pdf-generation) feature is responsible in populating data from Questionnaire Response to HTML. +::: + +## Sample JSON + +```json +{ + "trigger": "ON_CLICK", + "workflow": "LAUNCH_PDF_GENERATION", + "pdfConfig": { + "title": "Depression Report", + "titleSuffix": "James", + "structureReference": "Binary/b1234", + "subjectReference": "Patient/p1234", + "questionnaireReferences": [ + "Questionnaire/q1", + "Questionnaire/q2", + "Questionnaire/q3", + "Questionnaire/q4" + ] + } +} +``` + +## Config properties + +|Property | Description | Required | Default | +|--|--|:--:|:--:| +`title` | The saved PDF file title | No | `null` | +`titleSuffix` | The saved PDF file title suffix; usually contains the subject name | No | `null` | +`structureReference` | The Binary resource that contains HTML structure of the PDF | Yes | `null` | +`subjectReference` | The subject/patient/child who the answers from the Questionnaire Response apply to | Yes | `null` | +`questionnaireReferences` | The Questionnaire resource that is related to a Questionnaire Response which contains data to be populated into the PDF | Yes | `[]` | diff --git a/docs/engineering/app/configuring/pdf-generation.mdx b/docs/engineering/app/configuring/pdf-generation.mdx index 6ce55576b3..8f5d675ed6 100644 --- a/docs/engineering/app/configuring/pdf-generation.mdx +++ b/docs/engineering/app/configuring/pdf-generation.mdx @@ -1,9 +1,11 @@ # PDF Generation ## Overview -The PDF generation feature using the `HtmlPopulator` class simplifies the process of dynamically populating HTML templates with data from a QuestionnaireResponse, making it easy to generate customized content based on user responses. +The PDF generation feature is using the `HtmlPopulator` class that simplifies the process of dynamically populating HTML templates with data from a QuestionnaireResponse, making it easy to generate customized content based on user responses. -The `HtmlPopulator` class is utilized by replacing custom tags with data from a QuestionnaireResponse. It supports tags such as `@is-not-empty`, `@answer-as-list`, `@answer`, `@submitted-date`, and `@contains`. +The `HtmlPopulator` class is utilized by replacing custom tags with data from a QuestionnaireResponse. Currently supported tags are: `@is-not-empty`, `@answer-as-list`, `@answer`, `@submitted-date`, `@contains`, and `@is-questionnaire-submitted`. + +The `HtmlPopulator` class allows multiple QuestionnaireResponses to be populated into the HTML. Please use `questionnaireId/linkId` format to describe which linkId from which Questionnaire you want to retrieve the answer from. ## Usage Below are examples of how each custom tag can be used in an HTML template and the expected output. @@ -12,7 +14,7 @@ Below are examples of how each custom tag can be used in an HTML template and th #### Template HTML: ``` html -

        @is-not-empty('linkId')This content will be included if the answer exists.@is-not-empty('linkId')

        +

        @is-not-empty('questionnaireId/linkId')This content will be included if the answer exists.@is-not-empty('questionnaireId/linkId')

        ``` #### Explanation: @@ -23,7 +25,7 @@ The `@is-not-empty` tag checks if there is an answer for the specified `linkId`. #### Template HTML: ``` html
          - @answer-as-list('linkId') + @answer-as-list('questionnaireId/linkId')
        ``` @@ -34,7 +36,7 @@ The `@answer-as-list` tag will be replaced with a list of answers for the specif #### Template HTML: ``` html -

        The answer is: @answer('linkId')

        +

        The answer is: @answer('questionnaireId/linkId')

        ``` #### Explanation: @@ -44,35 +46,45 @@ The `@answer tag` will be replaced with the answer for the specified `linkId`. I #### Template HTML: ``` html -

        Submitted on: @submitted-date('MM/dd/yyyy')

        +

        Submitted on: @submitted-date('questionnaireId','MM/dd/yyyy')

        ``` #### Explanation: -The `@submitted-date` tag will be replaced with the formatted submission date. If no format is provided, a default date format will be used. +The `@submitted-date` tag will be replaced with the formatted submission date from the specified `questionnaireId`. If no format is provided, a default date format will be used. ### @contains #### Template HTML: ``` html -

        @contains('linkId', 'indicator')This content will be included if the indicator is found.@contains('linkId', 'indicator')

        +

        @contains('questionnaireId/linkId', 'indicator')This content will be included if the indicator is found.@contains('questionnaireId/linkId', 'indicator')

        ``` #### Explanation: The `@contains` tag checks if the specified `linkId` contains the given `indicator`. If the indicator is found, the content within the tags will be included in the final HTML. If the indicator is not found, the content will be removed. +### @is-questionnaire-submitted + +#### Template HTML: +``` html +

        @is-questionnaire-submitted('questionnaireId')This content will only show if the Questionnaire Response of the described Questionnaire exists.@is-questionnaire-submitted('questionnaireId')

        +``` + +#### Explanation: +The `@is-questionnaire-submitted` tag checks if the specified `questionnaireId` has been submitted i.e. the matching Questionnaire Response is passed to the HtmlPopulator class. If the Questionnaire Response is found, the content within the tags will be included in the final HTML. If the indicator is not found, the content will be removed. + ## Example ### Input HTML Template ``` html -

        @is-not-empty('name')Name: @answer('name')@is-not-empty('name')

        +

        @is-not-empty('Q123/name')Name: @answer('Q123/name')@is-not-empty('Q123/name')

        Hobbies:

          - @answer-as-list('hobbies') + @answer-as-list('Q123/hobbies')
        -

        Submitted on: @submitted-date('yyyy-MM-dd')

        -

        @contains('age', '30')This person is 30 years old.@contains('age', '30')

        +

        Submitted on: @submitted-date('Q123','yyyy-MM-dd')

        +

        @contains('Q123/age', '30')This person is 30 years old.@contains('Q123/age', '30')

        ```