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


##########
superset/dashboards/api.py:
##########
@@ -1574,6 +1590,123 @@ 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. Acquire a shared,
+        # atomic distributed lock (Redis when configured, the metadata DB
+        # otherwise) so the guard works across the web server and workers and 
is
+        # not a no-op under the default cache. The task releases it when it
+        # settles; the TTL is the backstop if that release is ever lost.
+        lock_params = export_lock_params(g.user.id, dashboard.id)
+        try:
+            AcquireDistributedLock(
+                EXPORT_LOCK_NAMESPACE,
+                lock_params,
+                ttl_seconds=EXPORT_LOCK_TTL_SECONDS,
+            ).run()
+        except LockAlreadyHeldException:
+            return self.response(
+                202,
+                message="An Excel export for this dashboard is already in 
progress.",
+            )

Review Comment:
   **Suggestion:** The endpoint documents `202` as returning a job identifier, 
but this `202` branch returns only a message when a lock is already held, which 
breaks response-contract consistency for clients expecting `job_id`. Return a 
different status (for example conflict) for duplicate in-flight exports or 
include a job identifier in this path too. [api mismatch]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ⚠️ Duplicate exports return 202 without documented job_id.
   - ⚠️ API clients relying job_id see inconsistent payload.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. From the dashboard UI, trigger an Excel export (frontend handler 
`onExportXlsx` in
   
`superset-frontend/src/dashboard/components/menu/DownloadMenuItems/index.tsx:31-41`
 issues
   `POST /api/v1/dashboard/<id>/export_xlsx/` with `active_data_mask` and 
`mode`).
   
   2. Ensure the first request is accepted: `DashboardRestApi.export_xlsx` in
   `superset/dashboards/api.py:94-209` enqueues the Celery task and returns
   `self.response(202, job_id=job_id)` (see lines 191-209), matching
   `DashboardExportXlsxResponseSchema.job_id` defined at
   `superset/dashboards/schemas.py:135-138`.
   
   3. While the first export task `export_dashboard_excel` (in
   `superset/tasks/export_dashboard_excel.py:7-87`) is still running and the 
lock has not yet
   been released (release happens in the `finally` block at lines 88-99), 
immediately trigger
   a second export for the same dashboard and user via the same endpoint.
   
   4. On this second request, `export_xlsx` attempts to acquire the distributed 
lock and hits
   the `except LockAlreadyHeldException` branch at 
`superset/dashboards/api.py:1684-1688`,
   returning `self.response(202, message="An Excel export for this dashboard is 
already in
   progress.")` without a `job_id`, even though the OpenAPI doc for the `202` 
response at
   lines 124-129 specifies `DashboardExportXlsxResponseSchema` (which only 
defines `job_id`).
   API clients relying on that schema will receive a `202` without the 
documented identifier.
   ```
   </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=da7e3976d24b4aeab89b3663e6a09cf8&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=da7e3976d24b4aeab89b3663e6a09cf8&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:** 1684:1688
   **Comment:**
        *Api Mismatch: The endpoint documents `202` as returning a job 
identifier, but this `202` branch returns only a message when a lock is already 
held, which breaks response-contract consistency for clients expecting 
`job_id`. Return a different status (for example conflict) for duplicate 
in-flight exports or include a job identifier in this path too.
   
   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=61358f7d3e43a0eeb0b037e041c49869556ebf6c7e6f13a027fd35265d25e63c&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41133&comment_hash=61358f7d3e43a0eeb0b037e041c49869556ebf6c7e6f13a027fd35265d25e63c&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