Skip to content

Conversation

@ab-smith
Copy link
Contributor

@ab-smith ab-smith commented Jan 5, 2026

supported tags for now:

  Text: {{audit_name}}, {{framework_name}}, {{date}}, {{authors}}, {{reviewers}}, {{contributors}}, {{igs}}, {{req_total}}, {{req_compliant}}, {{req_partially_compliant}}, {{req_non_compliant}}, {{req_not_applicable}}, {{req_not_assessed}}, {{ac_count}}, {{audit_description}}

  Charts: {{compliance_donut}}, {{completion_bar}}, {{compliance_radar}}, {{category_radar}}, {{chart_controls}}

  Tables: {{table_p1_controls}}, {{table_full_controls}}, {{table_category_scores}}, {{table_compliance_summary}}

Summary by CodeRabbit

  • New Features

    • PowerPoint (PPTX) export for compliance assessments, plus a new export option in the UI across EN/DE/ES/FR/IT/NL.
    • Direct download endpoint/handler so PPTX files can be streamed to users with a timestamped filename.
  • Improvements

    • Improved donut chart rendering in reports (cleaner slices, conditional percentages, legend/layout enhancements).

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 5, 2026

📝 Walkthrough

Walkthrough

Adds PPTX export end-to-end: backend generator produces PPTX-ready context and charts; viewset renders a PPTX template and streams the file; frontend adds a proxy route, UI export link, and translations; python-pptx added as a dependency.

Changes

Cohort / File(s) Summary
Backend Core Logic
backend/core/generators.py
New gen_audit_pptx_context(id, tree, lang) builds PPTX context: aggregated status counts, per-category scores, applied controls lists, translations, and chart image buffers (spider, radar/category, donut, horizontal bar, completion).
Backend PPTX Handling
backend/core/views.py
New pptx_report(self, request, pk) in ComplianceAssessmentViewSet loads language-specific PPTX template, replaces text placeholders, inserts chart images and dynamic tables (controls, scores), and streams the resulting PPTX. Adds imports: Presentation, Inches, and gen_audit_pptx_context.
Backend Dependencies
backend/pyproject.toml
Added runtime dependency: python-pptx = "^1.0.2".
Frontend Translations
frontend/messages/en.json, frontend/messages/de.json, frontend/messages/es.json, frontend/messages/fr.json, frontend/messages/it.json, frontend/messages/nl.json
Added asPPTX translation key for PowerPoint export in each locale (en/de/es/fr/it/nl).
Frontend SvelteKit Route
frontend/src/routes/(app)/(internal)/compliance-assessments/[id=uuid]/export/pptx/+server.ts
New GET handler proxies backend PPTX endpoint, returns 400 on fetch failure, sets Content-Type and Content-Disposition, and streams the PPTX with a timestamped filename.
Frontend UI
frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte
Added PPTX export link in the export popover (points to /compliance-assessments/{id}/export/pptx), alongside existing CSV/XLSX/Word/ZIP options.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Frontend UI
    participant Route as SvelteKit Route
    participant API as Backend ViewSet
    participant Gen as Generator
    participant PPTX as python-pptx

    User->>UI: Click "Export as PPTX"
    UI->>Route: GET /compliance-assessments/{id}/export/pptx
    Route->>API: Fetch pptx_report(pk)
    API->>Gen: gen_audit_pptx_context(pk, tree, lang)
    Gen->>Gen: Compute metrics, category scores, controls
    Gen-->>API: Return context + chart buffers
    API->>PPTX: Load template & replace placeholders
    API->>PPTX: Insert chart images and tables from buffers
    PPTX-->>API: Render PPTX bytes
    API-->>Route: Stream PPTX response
    Route-->>User: Download exec_report.pptx
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled code and stitched charts bright,

Slides now hum with audit light,
Seven tongues say "asPPTX" with cheer,
From generator to download, the rabbit hops near,
A tiny report that makes auditors cheer! 🥕📊

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: export audit as powerpoint deck' directly and clearly summarizes the primary change: adding PowerPoint export functionality for audit reports across multiple backend and frontend files.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Fix all issues with AI Agents 🤖
In @backend/core/generators.py:
- Around line 794-797: safe_translate can raise KeyError for unsupported lang
codes; update it to first guard against None/"--" keys, then fetch the
translations mapping with i18n_dict.get(lang, i18n_dict.get("en", {})) before
calling .get(key, key) so unsupported languages fall back to English (or an
empty dict) instead of crashing. Apply the same defensive lookup pattern to
gen_audit_context to avoid KeyError when accessing i18n_dict[lang].

In
@frontend/src/routes/(app)/(internal)/compliance-assessments/[id=uuid]/export/pptx/+server.ts:
- Line 14: The generated filename uses new Date().toISOString() which contains
colons (and dots) invalid on Windows; update the fileName construction in
+server.ts to sanitize the timestamp (e.g., replace colons and dots with safe
characters or use Date.now()). For example, change the fileName variable
assignment (const fileName = ...) to use new
Date().toISOString().replace(/[:.]/g, '-') or use Date.now() so the resulting
string has no illegal characters before appending .pptx.
🧹 Nitpick comments (6)
frontend/src/routes/(app)/(internal)/compliance-assessments/[id=uuid]/export/pptx/+server.ts (2)

10-12: Propagate the original backend error status instead of hardcoding 400.

The current implementation always returns a 400 error regardless of the actual backend response (could be 404, 403, 500, etc.). This makes debugging harder and provides inaccurate error information to the client.

🔎 Proposed fix
-	if (!res.ok) {
-		error(400, 'Error fetching the PPTX file');
-	}
+	if (!res.ok) {
+		error(res.status, 'Error fetching the PPTX file');
+	}

16-22: The Transfer-Encoding: chunked header is unnecessary.

When passing a ReadableStream (from res.body) to the Response constructor, the runtime automatically handles chunked transfer encoding. Manually setting this header is redundant and could potentially cause issues with some HTTP/2 implementations where chunked encoding is not used.

🔎 Proposed fix
 	return new Response(res.body, {
 		headers: {
 			'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
-			'Content-Disposition': `attachment; filename="${fileName}"`,
-			'Transfer-Encoding': 'chunked'
+			'Content-Disposition': `attachment; filename="${fileName}"`
 		}
 	});
backend/core/generators.py (3)

646-712: Significant code duplication with gen_audit_context.

The helper functions count_category_results, aggregate_category_scores, i18n_dict, and safe_translate are duplicated from gen_audit_context (lines 333-519). This violates DRY and increases maintenance burden.

Consider extracting these to module-level functions that both gen_audit_context and gen_audit_pptx_context can share.

🔎 Suggested approach
# Move to module level (before gen_audit_context)

I18N_DICT = {
    "en": {
        "compliant": "Compliant",
        # ... rest of translations
    },
    "fr": {
        "compliant": "Conformes",
        # ... rest of translations
    },
}

def safe_translate(lang: str, key: str) -> str:
    if key is None or key == "--":
        return "-"
    return I18N_DICT.get(lang, I18N_DICT["en"]).get(key, key)

def count_category_results(data):
    # ... existing implementation
    pass

def aggregate_category_scores(data):
    # ... existing implementation
    pass

Then both gen_audit_context and gen_audit_pptx_context can call these shared functions.


847-879: N+1 query performance issue when counting requirements per control.

Each iteration queries the database separately to count RequirementAssessment objects for each applied control. With many controls, this creates significant database overhead.

🔎 Proposed optimization using annotation
from django.db.models import Count, Q

# Replace the individual queries with a single annotated query
applied_controls = AppliedControl.objects.filter(
    requirement_assessments__in=requirement_assessments_objects
).distinct().annotate(
    requirements_count=Count(
        'requirement_assessments',
        filter=Q(requirement_assessments__compliance_assessment=audit)
    )
)

p1_controls = []
for ac in applied_controls.filter(priority=1):
    p1_controls.append({
        "name": ac.name,
        "description": safe_translate(lang, ac.description),
        "status": safe_translate(lang, ac.status),
        "category": safe_translate(lang, ac.category),
        "coverage": ac.requirements_count,  # Use annotated value
    })

full_controls = []
for ac in applied_controls:
    full_controls.append({
        "name": ac.name,
        "description": safe_translate(lang, ac.description),
        "prio": f"P{ac.priority}" if ac.priority else "-",
        "status": safe_translate(lang, ac.status),
        "eta": safe_translate(lang, ac.eta),
        "category": safe_translate(lang, ac.category),
        "coverage": ac.requirements_count,  # Use annotated value
    })

Note: The same N+1 issue exists in gen_audit_context (lines 572-605). Consider fixing both.


754-755: Missing error handling for empty requirements case.

Unlike gen_audit_context (line 474-475) which logs an error when no requirements are found (total == 0), this function silently continues. Consider adding similar handling for consistency and debugging.

🔎 Proposed fix
     total = sum([v for v in aggregated.values()])
+    if total == 0:
+        print("Error:: No requirements found, something is wrong. aborting ..")
     aggregated["total"] = total

Alternatively, consider raising an exception or returning an error response instead of just logging.

backend/core/views.py (1)

7843-8115: Solid PPTX export flow; consider a couple of defensive tweaks around context handling

The overall structure (language handling, template fallback, filtered tree, context generation, text/chart/table injection, streamed response) looks good and aligns with the existing word_report behavior.

Two areas worth tightening up:

  1. Robustness of category_scores handling

table_category_scores currently assumes context["category_scores"] is dict-like and calls .values(). The generator docstring describes category_scores as a “data list”, which may instead be a list of dicts; a mismatch would raise at runtime.

To make this resilient to either a list or a dict (and to a missing key), you can normalize before iterating:

Suggested change for table_category_scores block
-                "table_category_scores": {
-                    "headers": ["Category", "Average Score", "Items", "Scored"],
-                    "data": [
-                        [
-                            v["name"],
-                            str(v["average_score"]),
-                            str(v["item_count"]),
-                            str(v["scored_count"]),
-                        ]
-                        for v in context["category_scores"].values()
-                    ],
-                },
+                "table_category_scores": {
+                    "headers": ["Category", "Average Score", "Items", "Scored"],
+                    "data": [
+                        [
+                            v.get("name", ""),
+                            str(v.get("average_score", "")),
+                            str(v.get("item_count", "")),
+                            str(v.get("scored_count", "")),
+                        ]
+                        for v in (
+                            (context.get("category_scores") or {}).values()
+                            if isinstance(context.get("category_scores"), dict)
+                            else (context.get("category_scores") or [])
+                        )
+                    ],
+                },
  1. Guarding against missing/None chart buffers

If any of the chart buffers in chart_images can be None in some edge cases, item["chart_buffer"].seek(0) will raise. A small guard in the detection loop avoids hard failures:

Example guard around chart placeholders
-                    for chart_name, chart_buffer in chart_images.items():
+                    for chart_name, chart_buffer in chart_images.items():
+                        if chart_buffer is None:
+                            continue
                         placeholder = "{{" + chart_name + "}}"

Both are small, localized changes that make the export action more tolerant of context shape changes and partial data.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e14768a and 4878f6a.

⛔ Files ignored due to path filters (2)
  • backend/core/templates/core/audit_report_template_en.pptx is excluded by !**/*.pptx
  • backend/poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • backend/core/generators.py
  • backend/core/views.py
  • backend/pyproject.toml
  • frontend/messages/de.json
  • frontend/messages/en.json
  • frontend/messages/es.json
  • frontend/messages/fr.json
  • frontend/messages/it.json
  • frontend/messages/nl.json
  • frontend/src/routes/(app)/(internal)/compliance-assessments/[id=uuid]/export/pptx/+server.ts
  • frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte
🧰 Additional context used
🧬 Code graph analysis (3)
frontend/src/routes/(app)/(internal)/compliance-assessments/[id=uuid]/export/pptx/+server.ts (1)
frontend/src/lib/utils/constants.ts (1)
  • BASE_API_URL (4-8)
backend/core/views.py (1)
backend/core/generators.py (2)
  • gen_audit_context (332-643)
  • gen_audit_pptx_context (646-921)
backend/core/generators.py (1)
backend/core/models.py (3)
  • ComplianceAssessment (5711-6620)
  • AppliedControl (4058-4498)
  • RequirementAssessment (6623-6932)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build (3.12)
  • GitHub Check: build_community_frontend
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: test (3.12)
  • GitHub Check: Analyze (python)
🔇 Additional comments (9)
frontend/messages/nl.json (1)

269-272: asPPTX translation is consistent with existing export labels

The new "asPPTX": "als PowerPoint" string matches the pattern of "asPDF", "asCSV", and "asWord" and is syntactically valid JSON. No further changes needed.

frontend/messages/en.json (1)

354-359: English asPPTX label fits existing export options

"asPPTX": "as PowerPoint" is clear, consistent with nearby "asWord" / "asXLSX" labels, and JSON structure is correct. No changes required.

backend/pyproject.toml (1)

9-53: python-pptx dependency aligns with new PPTX export feature

Adding python-pptx = "^1.0.2" under runtime dependencies is appropriate for the new PPTX report generation and is consistent with the existing document/report tooling in this project. Just ensure the corresponding lockfile and deployment images/environments are updated so this dependency (and its transitive ones, like Pillow) are available at runtime.

frontend/messages/es.json (1)

269-272: PPTX label is consistent with existing export options

"asPPTX": "como PowerPoint" matches the pattern of asPDF/asCSV/asWord and uses correct Spanish phrasing and capitalization. Looks good.

frontend/messages/it.json (1)

269-272: Italian PPTX translation aligned with other formats

"asPPTX": "come PowerPoint" is consistent with asPDF/asCSV/asWord and is idiomatic Italian for the export option. No changes needed.

frontend/messages/fr.json (1)

350-350: LGTM! Translation is correct and well-placed.

The French translation "en PowerPoint" follows the same pattern as other export format translations (e.g., "en Word" on line 349) and is appropriately positioned among related export options.

frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte (1)

630-634: LGTM! PPTX export link is correctly implemented.

The new PPTX export option:

  • Follows the same URL pattern and styling as existing export options (CSV, XLSX, Word)
  • Is correctly placed within the permission gate for non-third-party users
  • Uses the proper translation function m.asPPTX()
  • Integrates seamlessly with the existing export menu structure
frontend/messages/de.json (1)

272-272: LGTM!

The German translation "als PowerPoint" follows the established naming convention (asXXX pattern) and is consistent with the other export format translations (asPDF, asCSV, asWord).

backend/core/views.py (1)

63-70: PPTX imports and generator wiring look appropriate

The new pptx imports and gen_audit_pptx_context integration are consistent with the existing Word report flow and stay nicely scoped to reporting. No issues spotted here.

Comment on lines +794 to +797
def safe_translate(lang: str, key: str) -> str:
if key is None or key == "--":
return "-"
return i18n_dict[lang].get(key, key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

safe_translate may fail for unsupported languages.

The function only supports 'en' and 'fr' in i18n_dict. If a different language code is passed, accessing i18n_dict[lang] will raise a KeyError.

🔎 Proposed fix with fallback
     def safe_translate(lang: str, key: str) -> str:
         if key is None or key == "--":
             return "-"
-        return i18n_dict[lang].get(key, key)
+        return i18n_dict.get(lang, i18n_dict["en"]).get(key, key)

Note: The same issue exists in gen_audit_context at line 519.

🤖 Prompt for AI Agents
In @backend/core/generators.py around lines 794-797, safe_translate can raise
KeyError for unsupported lang codes; update it to first guard against None/"--"
keys, then fetch the translations mapping with i18n_dict.get(lang,
i18n_dict.get("en", {})) before calling .get(key, key) so unsupported languages
fall back to English (or an empty dict) instead of crashing. Apply the same
defensive lookup pattern to gen_audit_context to avoid KeyError when accessing
i18n_dict[lang].

error(400, 'Error fetching the PPTX file');
}

const fileName = `audit-exec-summary-${new Date().toISOString()}.pptx`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Filename contains colons from ISO timestamp which are invalid on Windows.

The ISO timestamp format includes colons (e.g., 2024-01-05T20:13:30.000Z) which are invalid characters in Windows filenames. This could cause issues when users download the file on Windows systems.

🔎 Proposed fix to sanitize the filename
-	const fileName = `audit-exec-summary-${new Date().toISOString()}.pptx`;
+	const fileName = `audit-exec-summary-${new Date().toISOString().replace(/:/g, '-')}.pptx`;
📝 Committable suggestion

‼️ 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.

Suggested change
const fileName = `audit-exec-summary-${new Date().toISOString()}.pptx`;
const fileName = `audit-exec-summary-${new Date().toISOString().replace(/:/g, '-')}.pptx`;
🤖 Prompt for AI Agents
In
@frontend/src/routes/(app)/(internal)/compliance-assessments/[id=uuid]/export/pptx/+server.ts
around line 14, The generated filename uses new Date().toISOString() which
contains colons (and dots) invalid on Windows; update the fileName construction
in +server.ts to sanitize the timestamp (e.g., replace colons and dots with safe
characters or use Date.now()). For example, change the fileName variable
assignment (const fileName = ...) to use new
Date().toISOString().replace(/[:.]/g, '-') or use Date.now() so the resulting
string has no illegal characters before appending .pptx.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
backend/core/generators.py (2)

683-735: Extract duplicated helper functions to module level.

count_category_results, aggregate_category_scores, i18n_dict, and safe_translate are duplicated between gen_audit_context and gen_audit_pptx_context. Consider extracting these to module-level definitions to reduce maintenance burden and potential for drift.

🔎 Suggested approach
# At module level (before gen_audit_context)
I18N_DICT = {
    "en": { ... },
    "fr": { ... },
}

def _safe_translate(lang: str, key: str) -> str:
    if key is None or key == "--":
        return "-"
    return I18N_DICT.get(lang, I18N_DICT["en"]).get(key, key)

def _count_category_results(data):
    # ... existing implementation
    
def _aggregate_category_scores(data):
    # ... existing implementation

Then both gen_audit_context and gen_audit_pptx_context can call these shared helpers.


870-902: N+1 query pattern for counting requirement assessments.

Each iteration executes a separate RequirementAssessment.objects.filter(...).count() query. For audits with many applied controls, this can degrade performance significantly.

🔎 Consider using annotation
from django.db.models import Count

# Annotate the count directly on the queryset
applied_controls_annotated = applied_controls.annotate(
    requirements_count=Count(
        'requirementassessment',
        filter=Q(requirementassessment__compliance_assessment=audit)
    )
)

for ac in applied_controls_annotated.filter(priority=1):
    p1_controls.append({
        "name": ac.name,
        # ...
        "coverage": ac.requirements_count,
    })
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4878f6a and 6cef19f.

⛔ Files ignored due to path filters (1)
  • backend/core/templates/core/audit_report_template_en.pptx is excluded by !**/*.pptx
📒 Files selected for processing (2)
  • backend/core/generators.py
  • backend/core/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/core/views.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-02-10T17:04:11.726Z
Learnt from: nas-tabchiche
Repo: intuitem/ciso-assistant-community PR: 1332
File: frontend/src/lib/components/ModelTable/server/ModelTable.svelte:173-211
Timestamp: 2025-02-10T17:04:11.726Z
Learning: In the CISO Assistant codebase, the `unsafeTranslate` function is named "unsafe" because it does not fall back to the input string when the translation is undefined, unlike `safeTranslate`. The name does not indicate security concerns.

Applied to files:

  • backend/core/generators.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build (3.12)
  • GitHub Check: test (3.12)
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build_community_frontend
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: Analyze (python)
🔇 Additional comments (2)
backend/core/generators.py (2)

100-131: LGTM!

The donut chart improvements are well-implemented: conditional percentage display for readability, external legend placement, and proper tight layout with bbox_inches="tight" for clean output.


909-952: LGTM!

The context assembly is well-structured with clear sections for text data, statistics, chart buffers, and data lists. Perimeter handling properly guards against None values.

Comment on lines +91 to +95
# Filter out zero values to avoid cluttering
filtered_data = [(v, l, c) for v, l, c in zip(values, labels, plot_colors) if v > 0]
if filtered_data:
values, labels, plot_colors = zip(*filtered_data)
values, labels, plot_colors = list(values), list(labels), list(plot_colors)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Empty data after filtering may cause unexpected behavior.

If all values are zero, filtered_data will be empty, and zip(*filtered_data) produces nothing. The subsequent ax.pie() call with empty values may render incorrectly or raise warnings.

🔎 Proposed fix to handle all-zero case
     # Filter out zero values to avoid cluttering
     filtered_data = [(v, l, c) for v, l, c in zip(values, labels, plot_colors) if v > 0]
     if filtered_data:
         values, labels, plot_colors = zip(*filtered_data)
         values, labels, plot_colors = list(values), list(labels), list(plot_colors)
+    else:
+        # Keep original data if all values are zero to show empty state
+        pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants