Repository: ohcnetwork/care
Pull request #2881: Adding component field to questionnaire
Author: login: praffq
Created: 2025-03-01T12:26:09Z | Merged: 2025-03-04T16:59:28Z

## Proposed Changes

- Added component field to questionnaire

Fixes https://github.com/ohcnetwork/roadmap/issues/1#event-16944634953

_*Only PR's with test cases included and passing lint and test pipelines will be reviewed*_

@ohcnetwork/care-backend-maintainers @ohcnetwork/care-backend-admins


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Enhanced observation records now include structured component details, offering a more detailed view of recorded data.
  - Added options in questionnaire items to designate components, supporting richer data capture.

- **Refactor**
  - Improved the processing and display of observation values and validations, ensuring clearer and more consistent presentation in reports.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/care/emr/migrations/0020_observation_component.py b/care/emr/migrations/0020_observation_component.py
new file mode 100644
index 0000000000..24abe7c102
--- /dev/null
+++ b/care/emr/migrations/0020_observation_component.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2025-03-01 12:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('emr', '0019_consent_note'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='observation',
+            name='component',
+            field=models.JSONField(default=list),
+        ),
+    ]
diff --git a/care/emr/models/observation.py b/care/emr/models/observation.py
index 6bffafa438..49feff8081 100644
--- a/care/emr/models/observation.py
+++ b/care/emr/models/observation.py
@@ -29,5 +29,6 @@ class Observation(EMRBaseModel):
     questionnaire_response = models.ForeignKey(
         "emr.QuestionnaireResponse", on_delete=models.CASCADE, null=True
     )
+    component = models.JSONField(default=list)
 
     # TODO Add index for patient -> encounter -> system -> code
diff --git a/care/emr/resources/observation/spec.py b/care/emr/resources/observation/spec.py
index 8098d3992f..f99d0bce79 100644
--- a/care/emr/resources/observation/spec.py
+++ b/care/emr/resources/observation/spec.py
@@ -40,6 +40,14 @@ class ReferenceRange(BaseModel):
     text: str | None = None
 
 
+class Component(BaseModel):
+    value: QuestionnaireSubmitResultValue
+    interpretation: str | None = None
+    reference_range: list[ReferenceRange] = []
+    code: Coding | None = None
+    note: str = ""
+
+
 class BaseObservationSpec(EMRResource):
     __model__ = Observation
 
@@ -107,6 +115,8 @@ class BaseObservationSpec(EMRResource):
 
     questionnaire_response: UUID4 | None = None
 
+    component: list[Component] = []
+
 
 class ObservationSpec(BaseObservationSpec):
     data_entered_by_id: int
diff --git a/care/emr/resources/questionnaire/spec.py b/care/emr/resources/questionnaire/spec.py
index f35560528a..72c6805ad5 100644
--- a/care/emr/resources/questionnaire/spec.py
+++ b/care/emr/resources/questionnaire/spec.py
@@ -139,6 +139,7 @@ class Question(QuestionnaireBaseSpec):
     questions: list["Question"] = []
     formula: str | None = None
     styling_metadata: dict = {}
+    is_component: bool = False
 
     @field_validator("unit")
     @classmethod
diff --git a/care/emr/resources/questionnaire/utils.py b/care/emr/resources/questionnaire/utils.py
index 6ed7275051..1d4c89f9cc 100644
--- a/care/emr/resources/questionnaire/utils.py
+++ b/care/emr/resources/questionnaire/utils.py
@@ -11,6 +11,7 @@
 from care.emr.models.patient import Patient
 from care.emr.models.questionnaire import Questionnaire, QuestionnaireResponse
 from care.emr.registries.care_valueset.care_valueset import validate_valueset
+from care.emr.resources.common import Coding
 from care.emr.resources.observation.spec import ObservationSpec, ObservationStatus
 from care.emr.resources.questionnaire.spec import QuestionType
 
@@ -147,7 +148,7 @@ def validate_question_result(  # noqa : PLR0912
             "answer_value_set"
         ):
             for value in values:
-                if not value.value_code:
+                if not value.code:
                     errors.append(
                         {
                             "type": "type_error",
@@ -160,7 +161,13 @@ def validate_question_result(  # noqa : PLR0912
                 if "answer_value_set" in questionnaire:
                     try:
                         validate_valueset(
-                            "unit", questionnaire["answer_value_set"], value.value_code
+                            "",
+                            questionnaire["answer_value_set"],
+                            Coding(
+                                code=value.code,
+                                display=value.display,
+                                system=value.system,
+                            ),
                         )
                     except ValueError:
                         errors.append(
@@ -173,7 +180,7 @@ def validate_question_result(  # noqa : PLR0912
         # TODO : Validate for options created by user as well
         if questionnaire["type"] == QuestionType.quantity.value:
             for value in values:
-                if not value.value_quantity:
+                if not value.unit:
                     errors.append(
                         {
                             "type": "type_error",
@@ -187,9 +194,9 @@ def validate_question_result(  # noqa : PLR0912
                 if "answer_value_set" in questionnaire:
                     try:
                         validate_valueset(
-                            "unit",
+                            "",
                             questionnaire["answer_value_set"],
-                            value.value_quantity.code,
+                            value.code,
                         )
                     except ValueError:
                         errors.append(
@@ -203,9 +210,10 @@ def validate_question_result(  # noqa : PLR0912
 
 
 def create_observation_spec(questionnaire, responses, parent_id=None):
-    spec = {}
-    spec["status"] = ObservationStatus.final.value
-    spec["value_type"] = questionnaire["type"]
+    spec = {
+        "status": ObservationStatus.final.value,
+        "value_type": questionnaire["type"],
+    }
     if "category" in questionnaire:
         spec["category"] = questionnaire["category"]
     if "code" in questionnaire:
@@ -222,6 +230,7 @@ def create_observation_spec(questionnaire, responses, parent_id=None):
         and responses[questionnaire["id"]].values
         and responses[questionnaire["id"]].values[0]
     ):
+        observation = {}
         for value in responses[questionnaire["id"]].values:
             observation = spec.copy()
             observation["id"] = str(uuid.uuid4())
@@ -250,19 +259,42 @@ def create_observation_spec(questionnaire, responses, parent_id=None):
     return observations
 
 
+def create_components(questionnaire, responses):
+    components = []
+    observations = convert_to_observation_spec(
+        questionnaire, responses, is_component=True
+    )
+    # Convert from observation spec into component spec
+    # Need to handle how body site and method works in these cases
+    # These values need to be ignored when is_component is selected in the FE and in the validations
+    for observation in observations:
+        if "main_code" not in observation or "value" not in observation:
+            continue
+        component = {"value": observation["value"], "code": observation["main_code"]}
+        if "note" in observation:
+            component["note"] = observation.note
+        components.append(component)
+    return components
+
+
 def convert_to_observation_spec(
-    questionnaire_obj: Questionnaire, responses, parent_id=None
+    questionnaire, responses, parent_id=None, is_component=False
 ):
     constructed_observation_mapping = []
-    for question in questionnaire_obj.get("questions", []):
+    for question in questionnaire.get("questions", []):
         if question["type"] == QuestionType.group.value:
             observation = create_observation_spec(question, responses, parent_id)
-            sub_mapping = convert_to_observation_spec(
-                question, responses, observation[0]["id"]
-            )
-            if sub_mapping:
+            if not is_component and question.get("is_component", False):
+                components = create_components(question, responses)
+                observation[0]["component"] = components
                 constructed_observation_mapping.extend(observation)
-                constructed_observation_mapping.extend(sub_mapping)
+            else:
+                sub_mapping = convert_to_observation_spec(
+                    question, responses, observation[0]["id"]
+                )
+                if sub_mapping:
+                    constructed_observation_mapping.extend(observation)
+                    constructed_observation_mapping.extend(sub_mapping)
         elif question.get("code"):
             constructed_observation_mapping.extend(
                 create_observation_spec(question, responses, parent_id)
diff --git a/care/emr/templatetags/discharge_summary_utils.py b/care/emr/templatetags/discharge_summary_utils.py
index d0fb01fe76..e13a1012db 100644
--- a/care/emr/templatetags/discharge_summary_utils.py
+++ b/care/emr/templatetags/discharge_summary_utils.py
@@ -42,13 +42,11 @@ def discharge_disposition_display(value: str) -> str:
 
 @register.filter
 def observation_value_display(observation: Observation) -> str | None:
-    if observation.value.get("value_code", None):
-        return observation.value.value_code.get("display", None)
-    if observation.value.get("value_quantity", None):
-        unit: str = observation.value.value_quantity.get("unit", {}).get(
-            "display", None
-        )
-        value: float | None = observation.value.value_quantity.get("value", None)
+    if observation.value.get("display", None):
+        return observation.value.get("display", None)
+    if observation.value.get("unit", None):
+        unit: str = observation.value.get("unit", {}).get("display", None)
+        value: float | None = observation.value.get("value", None)
         value = int(value) if value and value.is_integer() else value
         return f"{value} {unit}" if unit else value
     return observation.value.get("value", None)


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>care/emr/models/observation.py (1)</summary><blockquote>

`32-32`: **Field added with mutable default - consider using a callable.**

While this works initially, using a mutable object like a list as a default argument can cause unexpected behavior in Django models. When multiple model instances are created without explicitly setting this field, they might end up sharing the same list instance.

Consider using a callable for the default:

```diff
-    component = models.JSONField(default=list)
+    component = models.JSONField(default=list)  # Django converts this to a callable internally
```

Or to be extremely explicit about it:

```diff
-    component = models.JSONField(default=list)
+    component = models.JSONField(default=lambda: list())
```

But I suppose I'll let it slide since Django handles this specific case correctly internally.

</blockquote></details>
<details>
<summary>care/emr/migrations/0020_observation_component.py (2)</summary><blockquote>

`9-9`: **Just a tiny quote style inconsistency.**

The codebase seems to prefer double quotes, but single quotes are used here.

```diff
-        ('emr', '0019_consent_note'),
+        ("emr", "0019_consent_note"),
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

9-9: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

---

`14-16`: **Another quote style inconsistency.**

It's almost like someone doesn't care about consistency in quotes.

```diff
-            model_name='observation',
-            name='component',
+            model_name="observation",
+            name="component",
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

14-14: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

15-15: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

</blockquote></details>
<details>
<summary>care/emr/resources/questionnaire/spec.py (1)</summary><blockquote>

`142-142`: **New boolean flag added without documentation.**

The `is_component` flag has been added but lacks descriptive documentation unlike other properties in this class.

Consider adding a descriptive Field with documentation:

```diff
-    is_component: bool = False
+    is_component: bool = Field(
+        default=False, 
+        description="Whether question should be treated as a component of an observation"
+    )
```

It would be nice if you explained the purpose of this field to future developers... just saying.

</blockquote></details>
<details>
<summary>care/emr/templatetags/discharge_summary_utils.py (2)</summary><blockquote>

`45-47`: **Enhanced logic for display retrieval, but could be more defensive**

The change to first check for a "display" key is a cleaner approach that aligns with the new component field structure. However, this assumes observation.value is always a valid dictionary. It might be worth adding a defensive check to handle cases where observation.value could be None.

```diff
-    if observation.value.get("display", None):
+    if observation.value and observation.value.get("display", None):
```

---

`48-49`: **Improved value extraction with more generic approach**

The code now uses a more generalized approach to extract unit and value, but there's an inconsistency in how null values are handled. `display` check returns the value directly, while the `unit` path doesn't directly handle nulls for `value`.

```diff
-        unit: str = observation.value.get("unit", {}).get("display", None)
-        value: float | None = observation.value.get("value", None)
+        unit: str = observation.value.get("unit", {}).get("display", None)
+        value: float | None = observation.value.get("value", None)
```

I mean... this is technically the same code, but I'm just pointing out that the value extraction logic could potentially be more consistent with the display check on line 45.

</blockquote></details>
<details>
<summary>care/emr/resources/questionnaire/utils.py (2)</summary><blockquote>

`281-281`: **Updated function signature with is_component parameter**

The function signature has been updated to include an `is_component` parameter, but there's no docstring update to explain its purpose or how it should be used.

```diff
 def convert_to_observation_spec(
     questionnaire, responses, parent_id=None, is_component=False
 ):
+    """
+    Convert questionnaire responses to observation specifications.
+    
+    Args:
+        questionnaire: The questionnaire containing questions
+        responses: The responses to the questionnaire
+        parent_id: Optional ID of the parent observation
+        is_component: Whether to treat the observations as components
+        
+    Returns:
+        A list of observation specifications
+    """
```

---

`287-297`: **Logic for component creation could be clearer**

The condition `if not is_component and question.get("is_component", False)` is a bit confusing. It's checking if the current call is not for a component, but the question is marked as a component. Adding a comment would make this clearer.

```diff
-            if not is_component and question.get("is_component", False):
+            # If this is a top-level conversion (not already a component) and this question
+            # is marked to be treated as a component, create and attach components
+            if not is_component and question.get("is_component", False):
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 039292f46010ff5f16c84fd349adc08661c18cc0 and d68a81f41f3838ec7461a7a58cb6fcb2f5b2a047.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `care/emr/migrations/0020_observation_component.py` (1 hunks)
* `care/emr/models/observation.py` (1 hunks)
* `care/emr/resources/observation/spec.py` (2 hunks)
* `care/emr/resources/questionnaire/spec.py` (1 hunks)
* `care/emr/resources/questionnaire/utils.py` (8 hunks)
* `care/emr/templatetags/discharge_summary_utils.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>care/emr/migrations/0020_observation_component.py</summary>

9-9: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

9-9: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

14-14: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

---

15-15: Single quotes found but double quotes preferred

Replace single quotes with double quotes

(Q000)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Test / Test

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>care/emr/resources/observation/spec.py (2)</summary>

`43-49`: **New Component class added with proper structure.**

The Component class correctly follows the pattern of other similar classes in the codebase and provides all needed fields for representing components.

---

`118-118`: <details>
<summary>❓ Verification inconclusive</summary>

**New component field added to BaseObservationSpec.**

This change properly aligns with the model field added in observation.py and uses the Component class defined above.


Have you verified that all code that consumes the Observation model has been updated to handle this new field? Run the following to check:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for code that might need to be updated to handle the new component field
rg "Observation\(" -A 5 -B 5
```

Length of output: 720

---



</details>

**New Component Field Update – Verification Confirmed (with caution)**

I ran the grep command to check for usages of `Observation(` and confirmed that the only references appear in `care/emr/models/observation.py`—where fields like `status`, `is_group`, `category`, etc., are defined—but there’s no evidence of code still expecting the old behavior regarding the new `component` field. The new field in `care/emr/resources/observation/spec.py` appears to align correctly with its model counterpart.

Please ensure that any integration or consumer code (beyond what the grep command can detect) has been tested to handle the new `component` field. Otherwise, everything looks in order.

</details>
<details>
<summary>care/emr/resources/questionnaire/utils.py (2)</summary>

`164-170`: **Improved valueset validation with proper Coding object**

The validation now properly uses the Coding object, which is more structured and type-safe than the previous approach. Good change.

---

`213-216`: **Simplified spec dictionary initialization**

The code now uses a more concise dictionary initialization, which improves readability. Nice improvement.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T16:59:35Z

Review comments:
diff_hunk: @@ -187,9 +194,9 @@ def validate_question_result(  # noqa : PLR0912
                 if "answer_value_set" in questionnaire:
                     try:
                         validate_valueset(
-                            "unit",
+                            "",
                             questionnaire["answer_value_set"],
-                            value.value_quantity.code,
+                            value.code,
                         )
path: care/emr/resources/questionnaire/utils.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Inconsistent parameter passing to validate_valueset**

While the previous change to validate_valueset properly uses a Coding object, this instance passes `value.code` directly. This inconsistency could cause validation issues or confusion for developers.

```diff
-                        validate_valueset(
-                            "",
-                            questionnaire["answer_value_set"],
-                            value.code,
-                        )
+                        validate_valueset(
+                            "",
+                            questionnaire["answer_value_set"],
+                            Coding(
+                                code=value.code,
+                                display=value.display,
+                                system=value.system,
+                            ),
+                        )
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                            "",
                            questionnaire["answer_value_set"],
                            Coding(
                                code=value.code,
                                display=value.display,
                                system=value.system,
                            ),
                        )
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-04T16:59:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -173,7 +180,7 @@
         # TODO : Validate for options created by user as well
         if questionnaire["type"] == QuestionType.quantity.value:
             for value in values:
-                if not value.value_quantity:
+                if not value.unit:
path: care/emr/resources/questionnaire/utils.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Updated property access for quantity validation**

Similar to the code change, the property access has been updated from `value.value_quantity` to `value.unit`. Make sure this is consistent with how the object is structured throughout the codebase.

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Find all references to value_quantity and unit in the codebase to ensure consistent usage

echo "Searching for value_quantity references:"
rg "value\.value_quantity" --glob "*.py" -A 2 -B 2

echo "Searching for value.unit references:"
rg "value\.unit" --glob "*.py" -A 2 -B 2
```

Length of output: 1243

---



</details>

**Action Required: Inconsistent Property Access Between `value.unit` and `value.value_quantity`**

The change to use `value.unit` in the condition (line 183 in `care/emr/resources/questionnaire/utils.py`) is a step in the right direction. However, the verification shows that other parts of the same file (e.g., the condition checking with `and value.value_quantity` and the assignment using `value.value_quantity.model_dump(...)`) still reference the old property. Please update these sections to use `value.unit` for consistency with the new object structure.

- **Check and update:**
  - The conditional that currently uses `and value.value_quantity`
  - The assignment setting `observation["value"] = value.value_quantity.model_dump(...)`

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-04T16:59:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -250,19 +259,42 @@
     return observations
 
 
+def create_components(questionnaire, responses):
+    components = []
+    observations = convert_to_observation_spec(
+        questionnaire, responses, is_component=True
+    )
+    # Convert from observation spec into component spec
+    # Need to handle how body site and method works in these cases
+    # These values need to be ignored when is_component is selected in the FE and in the validations
+    for observation in observations:
+        if "main_code" not in observation or "value" not in observation:
+            continue
+        component = {"value": observation["value"], "code": observation["main_code"]}
+        if "note" in observation:
+            component["note"] = observation.note
+        components.append(component)
+    return components
+
path: care/emr/resources/questionnaire/utils.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**New component creation function with incomplete implementation**

The new function handles the conversion of observations to components, but there's a TODO comment about body site and method that isn't implemented. Also, there's an issue on line 275 where `observation.note` is accessed as an attribute rather than via dictionary key.

```diff
- if "note" in observation:
-     component["note"] = observation.note
+ if "note" in observation:
+     component["note"] = observation["note"]
```

Also, consider documenting this function with docstrings to explain its purpose and parameters, especially since it's implementing a new feature.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def create_components(questionnaire, responses):
    components = []
    observations = convert_to_observation_spec(
        questionnaire, responses, is_component=True
    )
    # Convert from observation spec into component spec
    # Need to handle how body site and method works in these cases
    # These values need to be ignored when is_component is selected in the FE and in the validations
    for observation in observations:
        if "main_code" not in observation or "value" not in observation:
            continue
        component = {"value": observation["value"], "code": observation["main_code"]}
        if "note" in observation:
            component["note"] = observation["note"]
        components.append(component)
    return components
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-04T16:59:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,7 +148,7 @@
             "answer_value_set"
         ):
             for value in values:
-                if not value.value_code:
+                if not value.code:
path: care/emr/resources/questionnaire/utils.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Adjusted property access pattern to match new value structure**

The check has been updated from `value.value_code` to `value.code`, which seems to align with the new component field structure. Ensure this change is consistent with all other code that might be accessing this property.

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Find all references to value_code and code in the codebase to ensure consistent usage

echo "Searching for value_code references:"
rg "value\.value_code" --glob "*.py" -A 2 -B 2

echo "Searching for value.code references:"
rg "value\.code" --glob "*.py" -A 2 -B 2
```

Length of output: 2452

---



</details>

**Action Item: Fix Inconsistent Property Access in `utils.py`**

It looks like there’s still an inconsistency in how the value is being accessed. While the conditional now correctly checks `if not value.code:`, the assignment still calls `value.value_code.model_dump(...)` instead of using the new property. Please update this reference for consistency and verify that all similar usages across the codebase correctly use `value.code`.

- **File:** `care/emr/resources/questionnaire/utils.py`
  - Update line where `observation["value"] = value.value_code.model_dump(...)` is called.
  - Confirm that all property accesses follow the new structure (using `value.code`).

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-04T16:59:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-01T12:26:17Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

This set of changes introduces a new JSONField named `component` to the `observation` model via both a migration and a model update. Additionally, a new `Component` class and related attributes are added to the observation and questionnaire specifications to enable the handling of component data. The questionnaire utilities have been updated with revised validations, a new function for creating components, and adjustments to the observation spec conversion. Finally, the discharge summary display logic has been modified for clearer attribute access and formatting.

## Changes

| Files                                                                                                          | Change Summary                                                                                                                                                                                                                                                                             |
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `care/emr/migrations/0020_observation_component.py`<br>`care/emr/models/observation.py`                          | Added a new JSONField `component` with a default of an empty list to the observation model, along with necessary migration dependency updates.                                                                                                                                          |
| `care/emr/resources/observation/spec.py`<br>`care/emr/resources/questionnaire/spec.py`                          | Introduced a new `Component` class in the observation spec and added a new attribute `component` (list of `Component`) to `BaseObservationSpec`; added a new boolean attribute `is_component` in the questionnaire's `Question` class.                                             |
| `care/emr/resources/questionnaire/utils.py`                                                                    | Updated validation in `validate_question_result`, modifying attribute checks and valueset handling; added `create_components` function and updated `convert_to_observation_spec` (adding an `is_component` parameter) to better process questionnaire responses.                   |
| `care/emr/templatetags/discharge_summary_utils.py`                                                             | Revised the `observation_value_display` function to simplify logic by checking for a `display` key first and then for `unit` values, streamlining the formatting of observation values.                                                                                              |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Q as Questionnaire
    participant CT as convert_to_observation_spec
    participant CC as create_components
    participant OS as Observation Spec

    Q->>CT: Submit responses (with is_component flag)
    alt If is_component is true
        CT->>CC: Process components from responses
        CC-->>CT: Return list of components
    end
    CT->>OS: Integrate components into observation spec
    OS-->>Q: Return finalized observation spec
```

## Poem

> In our code’s quiet night, a field took flight,  
> A sneaky JSON charm now shines so bright.  
> Components dance in data’s gentle stream,  
> Migrations align, albeit as if on dream.  
> Observation whispers its refined symphony,  
> Even the skeptics must applaud this new harmony.  
> 😉✨

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-01T12:36:00Z
body: ## [Codecov](https://app.codecov.io/gh/ohcnetwork/care/pull/2881?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork) Report
Attention: Patch coverage is `28.94737%` with `27 lines` in your changes missing coverage. Please review.
> Project coverage is 56.31%. Comparing base [(`5d640ab`)](https://app.codecov.io/gh/ohcnetwork/care/commit/5d640abe6c66c4ed11ae0e788c621a22c34c95bd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork) to head [(`a641df9`)](https://app.codecov.io/gh/ohcnetwork/care/commit/a641df929c6b4a3a9e54d4ec81a21873a6e9e04f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork).
> Report is 2 commits behind head on develop.

| [Files with missing lines](https://app.codecov.io/gh/ohcnetwork/care/pull/2881?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork) | Patch % | Lines |
|---|---|---|
| [care/emr/resources/questionnaire/utils.py](https://app.codecov.io/gh/ohcnetwork/care/pull/2881?src=pr&el=tree&filepath=care%2Femr%2Fresources%2Fquestionnaire%2Futils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork#diff-Y2FyZS9lbXIvcmVzb3VyY2VzL3F1ZXN0aW9ubmFpcmUvdXRpbHMucHk=) | 8.33% | [22 Missing :warning: ](https://app.codecov.io/gh/ohcnetwork/care/pull/2881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork) |
| [care/emr/templatetags/discharge\_summary\_utils.py](https://app.codecov.io/gh/ohcnetwork/care/pull/2881?src=pr&el=tree&filepath=care%2Femr%2Ftemplatetags%2Fdischarge_summary_utils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork#diff-Y2FyZS9lbXIvdGVtcGxhdGV0YWdzL2Rpc2NoYXJnZV9zdW1tYXJ5X3V0aWxzLnB5) | 0.00% | [5 Missing :warning: ](https://app.codecov.io/gh/ohcnetwork/care/pull/2881?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #2881      +/-   ##
===========================================
- Coverage    56.34%   56.31%   -0.03%     
===========================================
  Files          226      226              
  Lines        11001    11025      +24     
  Branches      1118     1122       +4     
===========================================
+ Hits          6198     6209      +11     
- Misses        4786     4799      +13     
  Partials        17       17              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ohcnetwork/care/pull/2881?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ohcnetwork).

author_association: NONE