Skip to content

Commit

Permalink
Merge branch 'main' into fix-loading-nested-list-data
Browse files Browse the repository at this point in the history
  • Loading branch information
ellykits committed Oct 7, 2024
2 parents 66daf43 + 3b66b02 commit 2e8ef96
Show file tree
Hide file tree
Showing 26 changed files with 4,384 additions and 3,950 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 @@ -98,12 +98,29 @@ interface ConfigService {
description = "Search the status field"
}

return listOf(activeGroupSearchParameter, flagStatusSearchParameter)
val medicationSortSearchParameter =
SearchParameter().apply {
url = MEDICATION_SORT_URL
addBase("Medication")
name = SORT_SEARCH_PARAM
code = SORT_SEARCH_PARAM
type = Enumerations.SearchParamType.NUMBER
expression = "Medication.extension.where(url = '$MEDICATION_SORT_URL').value"
description = "Search the sort field"
}

return listOf(
activeGroupSearchParameter,
flagStatusSearchParameter,
medicationSortSearchParameter,
)
}

companion object {
const val ACTIVE_SEARCH_PARAM = "active"
const val APP_VERSION = "AppVersion"
const val STATUS_SEARCH_PARAM = "status"
const val SORT_SEARCH_PARAM = "sort"
const val MEDICATION_SORT_URL = "http://smartregister.org/SearchParameter/medication-sort"
}
}
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 @@ -22,6 +22,7 @@ enum class QuestionnaireType {
DEFAULT,
EDIT,
READ_ONLY,
SUMMARY,
}

fun QuestionnaireConfig.isDefault() =
Expand All @@ -32,3 +33,6 @@ fun QuestionnaireConfig.isEditable() =

fun QuestionnaireConfig.isReadOnly() =
QuestionnaireType.valueOf(this.type) == QuestionnaireType.READ_ONLY

fun QuestionnaireConfig.isSummary() =
QuestionnaireType.valueOf(this.type) == QuestionnaireType.SUMMARY
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'\\)",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.domain.model.RuleConfig
import org.smartregister.fhircore.engine.domain.model.ServiceMemberIcon
import org.smartregister.fhircore.engine.domain.model.ServiceStatus
import org.smartregister.fhircore.engine.rulesengine.services.DateService
import org.smartregister.fhircore.engine.rulesengine.services.LocationService
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
Expand Down Expand Up @@ -102,6 +103,7 @@ constructor(
put(DATA, mutableMapOf<String, Any>().apply { putAll(params) })
put(LOCATION_SERVICE, locationService)
put(SERVICE, rulesEngineService)
put(DATE_SERVICE, DateService)
}
if (repositoryResourceData != null) {
with(repositoryResourceData) {
Expand Down Expand Up @@ -711,6 +713,7 @@ constructor(
companion object {
private const val SERVICE = "service"
private const val LOCATION_SERVICE = "locationService"
private const val DATE_SERVICE = "dateService"
private const val INCLUSIVE_SIX_DIGIT_MINIMUM = 100000
private const val INCLUSIVE_SIX_DIGIT_MAXIMUM = 999999
private const val DEFAULT_REGEX = "(?<=^|,)[\\s,]*(\\w[\\w\\s]*)(?=[\\s,]*$|,)"
Expand Down
5 changes: 5 additions & 0 deletions android/engine/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<item name="questionnaireQuestionTextStyle">@style/TextAppearance.Material3.BodyLarge</item>
<item name="questionnaireMediaImageStyle">@style/AppTheme.MediaImageStyle</item>
<item name="questionnaireHelpHeaderStyle">@style/AppTheme.HelpHeaderStyle</item>
<item name="questionnaireNextButtonCircularProgressIndicatorStyle">@style/AppTheme.NextButtonCircularProgressIndicatorStyle</item>
<!-- Help card container color -->
<item
name="android:colorBackground"
Expand All @@ -79,6 +80,10 @@
<!-- /Help card container color -->
</style>

<style name="AppTheme.NextButtonCircularProgressIndicatorStyle" parent="Questionnaire.NextButtonCircularProgressIndicatorStyle">
<item name="indicatorColor">@color/colorPrimary</item>
</style>

<style name="AppTheme.QuestionnaireSubmitButtonStyle" parent="Widget.MaterialComponents.Button">
<item name="android:text">@string/str_save</item>
<item name="android:textAllCaps">true</item>
Expand Down
Loading

0 comments on commit 2e8ef96

Please sign in to comment.