codeant-ai-for-open-source[bot] commented on code in PR #41133:
URL: https://github.com/apache/superset/pull/41133#discussion_r3545215232


##########
tests/integration_tests/dashboards/api_tests.py:
##########
@@ -725,6 +726,7 @@ def test_info_security_dashboard(self):
             "can_write",
             "can_export",
             "can_export_as_example",
+            "can_export_xlsx",

Review Comment:
   **Suggestion:** The expected permission list includes `can_export_xlsx`, but 
the new endpoint is decorated with `@permission_name("export")`, so it reuses 
`can_export` rather than creating a new `can_export_xlsx` permission. This 
makes the test assert a permission that is never registered and will fail; 
update the expectation to match the actual permission mapping (or change the 
endpoint permission name if a distinct permission is intended). [api mismatch]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Dashboard permissions info test fails expecting non-existent permission.
   - ❌ CI pipeline blocked when integration tests run on PRs.
   - ⚠️ Permission documentation confused by mismatch between tests and RBAC.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Run the integration test `test_info_security_dashboard` in
   `tests/integration_tests/dashboards/api_tests.py:715-735`, which exercises 
the dashboard
   `_info` endpoint with `keys=["permissions"]`.
   
   2. The test logs in as `ADMIN_USERNAME` and calls `GET 
api/v1/dashboard/_info?q=...` via
   `self.get_assert_metric(uri, "info")`, then parses `data["permissions"]` 
from the
   response.
   
   3. The request is handled by `DashboardRestApi.info_headless` (defined in
   `superset/views/base_api.py:35-41` and inherited by 
`superset/dashboards/api.py`), which
   builds the permissions list from `DashboardRestApi.include_route_methods` in
   `superset/dashboards/api.py:250-274`.
   
   4. The Excel export endpoint `export_xlsx` at 
`superset/dashboards/api.py:1487-1491` is
   decorated with `@permission_name("export")`, so it contributes the existing 
`can_export`
   permission rather than a new `can_export_xlsx` permission; since 
`can_export_xlsx` only
   appears in the test expectation at 
`tests/integration_tests/dashboards/api_tests.py:729`
   and is never registered anywhere else, the returned `data["permissions"]` 
set does not
   contain `"can_export_xlsx"`, causing the assertion to fail and the test to 
error.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=9882931c0c134ffca919ff6e0c22f852&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=9882931c0c134ffca919ff6e0c22f852&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** tests/integration_tests/dashboards/api_tests.py
   **Line:** 729:729
   **Comment:**
        *Api Mismatch: The expected permission list includes `can_export_xlsx`, 
but the new endpoint is decorated with `@permission_name("export")`, so it 
reuses `can_export` rather than creating a new `can_export_xlsx` permission. 
This makes the test assert a permission that is never registered and will fail; 
update the expectation to match the actual permission mapping (or change the 
endpoint permission name if a distinct permission is intended).
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41133&comment_hash=b7da08561e0be5fca46fae2c5fa211e15a3b2618b7526b3eb1dbd60ca3e2e15b&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41133&comment_hash=b7da08561e0be5fca46fae2c5fa211e15a3b2618b7526b3eb1dbd60ca3e2e15b&reaction=dislike'>👎</a>



##########
superset/dashboards/api.py:
##########
@@ -1471,6 +1484,109 @@ def export_as_example(self, pk: int) -> Response:
             response.set_cookie(token, "done", max_age=600)
         return response
 
+    @expose("/<pk>/export_xlsx/", methods=("POST",))
+    @protect()
+    @safe
+    @permission_name("export")
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: 
f"{self.__class__.__name__}.export_xlsx",
+        log_to_statsd=False,
+    )
+    def export_xlsx(self, pk: int) -> WerkzeugResponse:
+        """Export all of a dashboard's chart data to an Excel workbook (async).
+        ---
+        post:
+          summary: Export dashboard chart data to Excel
+          description: >-
+            Enqueues an async task that writes each chart's data to its own
+            worksheet, uploads the .xlsx to S3, and emails the requesting user 
a
+            pre-signed download link. Returns immediately with a job id.
+          parameters:
+          - in: path
+            schema:
+              type: integer
+            name: pk
+            description: The dashboard id
+          requestBody:
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/DashboardExportXlsxPostSchema'
+          responses:
+            202:
+              description: Export task accepted
+              content:
+                application/json:
+                  schema:
+                    $ref: 
'#/components/schemas/DashboardExportXlsxResponseSchema'
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            403:
+              $ref: '#/components/responses/403'
+            404:
+              $ref: '#/components/responses/404'
+            500:
+              $ref: '#/components/responses/500'
+            501:
+              description: Excel export is not configured on this server
+        """
+        if not current_app.config["EXCEL_EXPORT_S3_BUCKET"]:
+            return self.response(
+                501, message="Excel export is not configured on this server."
+            )
+        try:
+            # Tolerate an empty/non-JSON body (e.g. a POST with no 
Content-Type);
+            # request.json would otherwise raise 415.
+            payload = DashboardExportXlsxPostSchema().load(
+                request.get_json(silent=True) or {}
+            )
+        except ValidationError as error:
+            return self.response_400(message=error.messages)
+
+        dashboard = cast(Dashboard, self.datamodel.get(pk, self._base_filters))
+        if not dashboard:
+            return self.response_404()
+        try:
+            security_manager.raise_for_access(dashboard=dashboard)
+        except SupersetSecurityException:
+            return self.response_403()
+
+        # Email delivery is the only result channel, so an account with an 
email
+        # address is required; embedded guest users are excluded in this 
version.
+        if isinstance(g.user, GuestUser) or not getattr(g.user, "email", None):
+            return self.response_400(
+                message="Excel export requires an account with an email 
address."
+            )
+        if not dashboard.slices:
+            return self.response_400(message="Dashboard has no charts to 
export.")
+
+        # Throttle: one concurrent export per user+dashboard. The lock is set
+        # here and cleared by the task; its TTL guards against a lost cleanup.
+        inflight_key = f"excel-export-inflight:{g.user.id}:{dashboard.id}"
+        if cache_manager.cache.get(inflight_key):
+            return self.response(
+                202,
+                message="An Excel export for this dashboard is already in 
progress.",
+            )

Review Comment:
   **Suggestion:** The endpoint advertises a `job_id` response schema for 
`202`, but the duplicate-in-flight branch returns only a message, breaking API 
contract consistency for clients that expect a correlation id. Return the 
existing job id from cache in this branch (or use a different status 
code/schema for “already running”). [api mismatch]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ API contract misleads clients expecting job_id in 202.
   - ⚠️ Integrations need custom handling for "already running" responses.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Note the OpenAPI description on `export_xlsx` in 
`superset/dashboards/api.py:97-135`,
   where the `202` response references
   `#/components/schemas/DashboardExportXlsxResponseSchema`.
   
   2. Inspect `DashboardExportXlsxResponseSchema` in 
`superset/dashboards/schemas.py:20-23`,
   which defines a single `job_id` field described as “Correlation id for the 
async export
   task.”
   
   3. Trigger an export for a dashboard (configured as in previous steps) so 
the first `POST
   /api/v1/dashboard/<pk>/export_xlsx/` sets an in-flight lock and enqueues a 
job; while that
   job is still running and the lock exists, send another POST to the same 
endpoint as the
   same user.
   
   4. The second request hits the throttle logic at 
`superset/dashboards/api.py:1568-174`;
   since `cache_manager.cache.get(inflight_key)` is truthy, it returns 
`self.response(202,
   message="An Excel export for this dashboard is already in progress.",)` 
without a `job_id`
   field, even though the documented `202` schema 
(`DashboardExportXlsxResponseSchema`) only
   defines `job_id`; clients built against the schema expecting `job_id` for 
all `202`
   responses must special-case this branch or will fail to correlate the 
already-running job.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=6b464ff96b1d4ca6b3bb98dd9a1d68fd&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=6b464ff96b1d4ca6b3bb98dd9a1d68fd&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/dashboards/api.py
   **Line:** 1569:1573
   **Comment:**
        *Api Mismatch: The endpoint advertises a `job_id` response schema for 
`202`, but the duplicate-in-flight branch returns only a message, breaking API 
contract consistency for clients that expect a correlation id. Return the 
existing job id from cache in this branch (or use a different status 
code/schema for “already running”).
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41133&comment_hash=2779f60c6d2f1e2a531d957436f746d97f5a92a6403c259809099e252c226392&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41133&comment_hash=2779f60c6d2f1e2a531d957436f746d97f5a92a6403c259809099e252c226392&reaction=dislike'>👎</a>



##########
superset/dashboards/excel_export/screenshot.py:
##########
@@ -0,0 +1,98 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+"""
+Render a single dashboard chart to a PNG for the image-mode Excel export.
+
+This reuses the same headless render path scheduled reports use
+(:class:`~superset.utils.screenshots.ChartScreenshot`), but points it at an
+Explore URL whose ``form_data`` carries the live dashboard filter state — so an
+embedded image reflects the same filters the data path applies, rather than the
+chart's default saved state.
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from flask import current_app
+
+from superset.charts.data.dashboard_filter_context import (
+    get_dashboard_filter_context,
+)
+from superset.utils import json
+from superset.utils.screenshots import ChartScreenshot
+from superset.utils.urls import get_url_path
+
+logger = logging.getLogger(__name__)
+
+
+def render_chart_image(
+    chart: Any,
+    dashboard_id: int,
+    active_data_mask: dict[str, Any],
+    user: Any,
+) -> bytes | None:
+    """
+    Render ``chart`` (as seen on ``dashboard_id``) to PNG bytes.
+
+    The chart is rendered through Explore in standalone mode with the live
+    dashboard filter state injected as ``extra_form_data`` — the same object 
the
+    data path merges into the query context — so the image and the data stay
+    consistent.
+
+    :param chart: The ``Slice`` to render
+    :param dashboard_id: The dashboard the chart is displayed on (for filter 
scope)
+    :param active_data_mask: Live dashboard filter state keyed by native 
filter id
+    :param user: The requesting user; the render runs with their permissions
+    :returns: PNG bytes, or ``None`` if the render failed (the caller skips and
+        notes the chart)
+    """
+    try:
+        filter_context = get_dashboard_filter_context(
+            dashboard_id=dashboard_id,
+            chart_id=chart.id,
+            active_data_mask=active_data_mask,
+        )
+
+        # Start from the chart's saved form data and force the slice id, then
+        # layer the live filters on top so the render matches the data path.
+        form_data: dict[str, Any] = json.loads(chart.params or "{}")
+        form_data["slice_id"] = chart.id
+        if filter_context.extra_form_data:
+            form_data["extra_form_data"] = filter_context.extra_form_data

Review Comment:
   **Suggestion:** This assignment overwrites any chart-saved `extra_form_data` 
instead of merging, which can drop chart-level filters/time settings and make 
image exports diverge from data exports. Merge dashboard filter context into 
existing `form_data["extra_form_data"]` rather than replacing it. [logic error]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Image-mode exports may omit chart-level extra_form_data constraints.
   - ⚠️ Data and image sheets can diverge, confusing dashboard consumers.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Trigger an Excel export in image mode by calling `POST
   /api/v1/dashboard/<pk>/export_xlsx/` with `{"active_data_mask": ..., "mode": 
"images"}` as
   the frontend does in
   
`superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx:31-36`;
 this
   enqueues `export_dashboard_excel` with `mode="images"`
   (`superset/tasks/export_dashboard_excel.py:251-258`).
   
   2. In the task, `_build_workbook` at 
`superset/tasks/export_dashboard_excel.py:168-200`
   iterates charts; for charts rendered as images (`_renders_as_image` at lines 
96-98), it
   calls `_write_chart_image_sheet`, which in turn calls 
`render_chart_image(chart,
   dashboard.id, active_data_mask, user)`
   (`superset/tasks/export_dashboard_excel.py:101-116`).
   
   3. `render_chart_image` in 
`superset/dashboards/excel_export/screenshot.py:44-78` loads
   the chart's saved form data JSON from `chart.params` (line 74), sets
   `form_data["slice_id"] = chart.id`, and then, when 
`filter_context.extra_form_data` is
   present, unconditionally assigns `form_data["extra_form_data"] =
   filter_context.extra_form_data` at lines 76-77, overwriting any 
`extra_form_data` already
   saved with the chart.
   
   4. For charts that rely on saved `extra_form_data`—for example, multi-layer 
visualizations
   where `_apply_layer_filtering` in `superset/viz.py:35-63` processes
   `self.form_data["extra_form_data"]` to produce layer-specific filters—this 
overwrite drops
   chart-level `extra_form_data` instead of merging dashboard filter 
contributions into it;
   the Explore URL built at 
`superset/dashboards/excel_export/screenshot.py:79-82` therefore
   uses only dashboard filter context, so the rendered PNG in image-mode 
exports can diverge
   from the data path (which applies dashboard filters via 
`apply_dashboard_filter_context`
   at `superset/tasks/export_dashboard_excel.py:138-145` while preserving 
non-overridden
   fields), leading to embedded images that omit chart constraints present in 
exported data
   sheets.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=262fa4c2bef44ce085b0c24351431470&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=262fa4c2bef44ce085b0c24351431470&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/dashboards/excel_export/screenshot.py
   **Line:** 76:77
   **Comment:**
        *Logic Error: This assignment overwrites any chart-saved 
`extra_form_data` instead of merging, which can drop chart-level filters/time 
settings and make image exports diverge from data exports. Merge dashboard 
filter context into existing `form_data["extra_form_data"]` rather than 
replacing it.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41133&comment_hash=f8ca1f498e007b190d0f474368a67b1fd5122d66cd7994dec3129ad62e8a610e&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41133&comment_hash=f8ca1f498e007b190d0f474368a67b1fd5122d66cd7994dec3129ad62e8a610e&reaction=dislike'>👎</a>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to