Skip to content

Commit

Permalink
Refactor PDF config properties in QuestionnaireConfig (#3498)
Browse files Browse the repository at this point in the history
* Refactor to use PdfConfig

Initially using QuestionnaireConfig for simplicity.

* Process multi QRs in HtmlPopulator

* Add new tag to check if Questionnaire has been submitted

* Remove subjectType since subjectReference is used

* Fix test and spotless

* Address review

* spotless

* Cleanup

* spotless

* Fix test

---------

Co-authored-by: Elly Kitoto <[email protected]>
  • Loading branch information
FikriMilano and ellykits authored Sep 27, 2024
1 parent a5c2fec commit ebd0355
Show file tree
Hide file tree
Showing 12 changed files with 707 additions and 407 deletions.
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(),
) : java.io.Serializable, Parcelable {

fun interpolate(computedValuesMap: Map<String, Any>) =
this.copy(
title = title?.interpolate(computedValuesMap),
titleSuffix = titleSuffix?.interpolate(computedValuesMap),
structureReference = structureReference?.interpolate(computedValuesMap),
subjectReference = subjectReference?.interpolate(computedValuesMap),
questionnaireReferences = questionnaireReferences.map { it.interpolate(computedValuesMap) },
)
}
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,
) : 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 {
Expand All @@ -155,12 +181,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 @@ -178,7 +205,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 @@ -199,14 +226,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

0 comments on commit ebd0355

Please sign in to comment.