Repository: MultiQC/MultiQC (2025-07-01T02:10:47Z)

Issue:
Custom content description missing when grouping sections
### Description of bug

When using custom content, the "description" of the child section is missing when a parent is specified. (To be exact, this  happens when `parent_name` is set, even if no `parent_id` or `parent_description` are specified).

The documentation for this feature: https://docs.seqera.io/multiqc/custom_content#grouping-sections-and-subsections

### File that triggers the error

[minimal example.zip](https://github.com/user-attachments/files/20345024/minimal.example.zip)

### MultiQC Error log

```console
multiqc -f .

/// MultiQC 🔍 v1.28

       file_search | Search path: ~/tmp/multiqc-bug/with-parent
         searching | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 2/2  
    custom_content | mqc_expression_unstranded: Found 1 samples (PlotType.TABLE)
     write_results | Data        : multiqc_data   (overwritten)
     write_results | Report      : multiqc_report.html   (overwritten)
           multiqc | MultiQC complete
```

### Before submitting

- [x] I have read the [troubleshooting documentation](https://docs.seqera.io/multiqc/usage/troubleshooting/).
- [x] I am using the latest release of MultiQC.
- [x] I have included a full MultiQC log, not truncated.
- [x] I have attached an input file (**.zip** if necessary) that triggers the error.


Discussion:

Patch:
diff --git a/multiqc/core/special_case_modules/custom_content.py b/multiqc/core/special_case_modules/custom_content.py
index 23ad327b1c..71459c0bd5 100644
--- a/multiqc/core/special_case_modules/custom_content.py
+++ b/multiqc/core/special_case_modules/custom_content.py
@@ -341,7 +341,7 @@ def custom_module_classes() -> List[BaseMultiqcModule]:
         else:
             # Is this file asking to be a sub-section under a parent section?
             mod_id = cast(ModuleId, ccdict.config.get("parent_id", ccdict.config.get("id", mod_id)))
-            section_id: SectionId = cast(SectionId, ccdict.config.get("section_id", mod_id))
+            section_id: SectionId = cast(SectionId, ccdict.config.get("section_id", ccdict.config.get("id", mod_id)))
 
             mod_anchor: Optional[Anchor] = None
             if "parent_anchor" in ccdict.config:
@@ -599,6 +599,7 @@ def add_cc_section(self, section_id: SectionId, section_anchor: Anchor, ccdict:
                 name=section_name,
                 anchor=section_anchor,
                 id=section_id,
+                description=ccdict.config.get("description", ""),
                 plot=plot,
                 content=content or "",
             )


Test patch:
diff --git a/tests/test_custom_content.py b/tests/test_custom_content.py
index 1ab68bb6bf..7df0a1dcb5 100644
--- a/tests/test_custom_content.py
+++ b/tests/test_custom_content.py
@@ -822,3 +822,86 @@ def test_markdown_front_matter_config(tmp_path):
 
     # Ensure markdown converted to HTML
     assert "<h1>Heading</h1>" in section.content or "<h1>Heading" in section.content
+
+
+def test_parent_grouping_with_description(tmp_path):
+    """Test that descriptions are properly passed to sections when using parent grouping."""
+    # Create two files with the same parent_id but different descriptions
+    file1 = tmp_path / "sample1_mqc.txt"
+    file1.write_text(
+        """\
+#parent_id: "my_group"
+#parent_name: "My Grouped Data"
+#parent_description: "This is the parent description"
+#id: "section1"
+#section_name: "Section 1"
+#description: "This is the description for section 1"
+#plot_type: "table"
+Sample,Value
+sample1,10
+"""
+    )
+
+    file2 = tmp_path / "sample2_mqc.txt"
+    file2.write_text(
+        """\
+#parent_id: "my_group"
+#id: "section2"
+#section_name: "Section 2"
+#description: "This is the description for section 2"
+#plot_type: "table"
+Sample,Value
+sample2,20
+"""
+    )
+
+    report.analysis_files = [file1, file2]
+    report.search_files(["custom_content"])
+    modules = custom_module_classes()
+
+    # Should have one module with two sections
+    assert len(modules) == 1
+    module = modules[0]
+    assert module.name == "My Grouped Data"
+    assert module.info == "This is the parent description"
+    assert len(module.sections) == 2
+
+    # Debug: print what sections we actually have
+    print(f"Module sections: {[(s.id, s.name) for s in module.sections]}")
+
+    # Check that both sections have their descriptions (HTML-wrapped)
+    section1 = next(s for s in module.sections if s.id == "section1")
+    section2 = next(s for s in module.sections if s.id == "section2")
+
+    assert section1.description == "<p>This is the description for section 1</p>"
+    assert section2.description == "<p>This is the description for section 2</p>"
+
+
+def test_parent_name_without_parent_description(tmp_path):
+    """Test that descriptions work when parent_name is set but parent_description is not."""
+    file1 = tmp_path / "sample1_mqc.txt"
+    file1.write_text(
+        """\
+#parent_name: "My Grouped Data"
+#id: "section1"
+#section_name: "Section 1"
+#description: "This is the description for section 1"
+#plot_type: "table"
+Sample,Value
+sample1,10
+"""
+    )
+
+    report.analysis_files = [file1]
+    report.search_files(["custom_content"])
+    modules = custom_module_classes()
+
+    # Should have one module with one section
+    assert len(modules) == 1
+    module = modules[0]
+    assert module.name == "My Grouped Data"
+    assert len(module.sections) == 1
+
+    # Check that the section has its description (HTML-wrapped)
+    section1 = module.sections[0]
+    assert section1.description == "<p>This is the description for section 1</p>"