Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor PDF config properties in QuestionnaireConfig #3498

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -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<String> = emptyList(),

Check warning on line 31 in android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt#L26-L31

Added lines #L26 - L31 were not covered by tests
) : java.io.Serializable, Parcelable {

fun interpolate(computedValuesMap: Map<String, Any>) =
this.copy(

Check warning on line 35 in android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt#L35

Added line #L35 was not covered by tests
title = title?.interpolate(computedValuesMap),
titleSuffix = titleSuffix?.interpolate(computedValuesMap),
structureReference = structureReference?.interpolate(computedValuesMap),
subjectReference = subjectReference?.interpolate(computedValuesMap),
questionnaireReferences = questionnaireReferences.map { it.interpolate(computedValuesMap) },
)

Check warning on line 41 in android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/PdfConfig.kt#L40-L41

Added lines #L40 - L41 were not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ data class QuestionnaireConfig(
val managingEntityRelationshipCode: String? = null,
val uniqueIdAssignment: UniqueIdAssignmentConfig? = null,
val linkIds: List<LinkIdConfig>? = null,
val htmlBinaryId: String? = null,
val htmlTitle: String? = null,
FikriMilano marked this conversation as resolved.
Show resolved Hide resolved
) : java.io.Serializable, Parcelable {

fun interpolate(computedValuesMap: Map<String, Any>) =
Expand Down Expand Up @@ -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),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, Any> = emptyMap()): Bundle =
Bundle().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<QuestionnaireResponse>,
) {
private var answerMap: Map<String, List<QuestionnaireResponseItemAnswerComponent>>
private var submittedDateMap: Map<String, Date>
private var questionnaireIds: List<String>

// 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<String, List<QuestionnaireResponseItemAnswerComponent>>()
val submittedDateMap = mutableMapOf<String, Date>()
val questionnaireIds = mutableListOf<String>()

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.
Expand Down Expand Up @@ -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++
}
}
Expand All @@ -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.
Expand All @@ -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 ->
"<li>${answer.value.valueToString()}</li>"
}
html.replace(i, matcher.end() + i, answerAsList)
Expand All @@ -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)
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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'\\)",
)
}
}
Loading
Loading