Skip to content

fix: export for non-owners with site permissions - #25

Merged
metaforx merged 1 commit into
mainfrom
fix/t27-db-store-export-inline-button
May 4, 2026
Merged

fix: export for non-owners with site permissions#25
metaforx merged 1 commit into
mainfrom
fix/t27-db-store-export-inline-button

Conversation

@metaforx

@metaforx metaforx commented May 4, 2026

Copy link
Copy Markdown
Owner
  • fix export action with site permissions

Summary by Sourcery

Expose a project-owned admin export endpoint for saved form data and wire the inline "Export entries" button to it, ensuring it respects site-scoped querysets and existing permissions.

Bug Fixes:

  • Ensure the db_store inline "Export entries" button exports entries for non-owner staff users instead of returning empty CSVs.
  • Validate and guard the export endpoint input and permissions, returning appropriate HTTP errors for invalid or unauthorized access.

Enhancements:

  • Route the export inline action through the admin's queryset to honor site scoping and reuse the existing bulk export behavior.

Documentation:

  • Document task T27 detailing the problem, constraints, implementation plan, and acceptance criteria for fixing the db_store "Export entries" inline button.

Tests:

  • Add regression tests covering the new export endpoint, inline button URL, permission enforcement, queryset scoping, and input validation.

…point, ensuring correct CSV export for all staff users
@sourcery-ai

sourcery-ai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a project-owned admin export endpoint for SavedFormDataEntry that reuses the existing admin export_data action with proper permissions and site scoping, rewires the inline "Export entries" button to target this endpoint, and introduces regression tests covering delegation, permissions, input validation, and site scope behavior.

Sequence diagram for the new SavedFormDataEntry export endpoint

sequenceDiagram
    actor StaffUser
    participant AdminInline
    participant DjangoAdminSite
    participant SavedFormDataEntryAdmin
    participant ExportEndpoint as Export_for_form_entry
    participant ExportAction as Export_data

    StaffUser->>AdminInline: Click Export_entries button
    AdminInline->>AdminInline: Build action_url with reverse(admin:unfold_fobi_savedformdataentry_export)
    AdminInline-->>StaffUser: Render link with form_entry_id

    StaffUser->>DjangoAdminSite: GET /admin/.../export/?form_entry_id=id
    DjangoAdminSite->>SavedFormDataEntryAdmin: admin_view(export_for_form_entry)

    SavedFormDataEntryAdmin->>ExportEndpoint: export_for_form_entry(request)
    ExportEndpoint->>SavedFormDataEntryAdmin: has_view_permission(request)
    SavedFormDataEntryAdmin-->>ExportEndpoint: bool
    alt No view permission
        ExportEndpoint-->>StaffUser: PermissionDenied (403)
    else Has view permission
        ExportEndpoint->>ExportEndpoint: Read form_entry_id from request.GET
        alt Missing or invalid form_entry_id
            ExportEndpoint-->>StaffUser: HttpResponseBadRequest (400)
        else Valid form_entry_id
            ExportEndpoint->>SavedFormDataEntryAdmin: get_queryset(request)
            SavedFormDataEntryAdmin-->>ExportEndpoint: Scoped queryset
            ExportEndpoint->>ExportEndpoint: queryset.filter(form_entry_id=form_entry_id)
            ExportEndpoint->>ExportAction: export_data(request, queryset)
            ExportAction-->>StaffUser: CSV response
        end
    end
Loading

Class diagram for SavedFormDataEntry admin export integration

classDiagram
    class SavedFormDataEntryAdminIntegrationMixin {
        +has_view_permission(request) bool
        +has_change_permission(request, obj) bool
        +has_delete_permission(request, obj) bool
        +get_urls() list
        +export_for_form_entry(request) HttpResponse
        +export_data(request, queryset) HttpResponse
    }

    class SavedFormDataEntryAdmin {
    }

    class FormHandlerEntryInline {
        +handler_actions(obj) list
    }

    class DjangoAdminSite {
        +admin_view(view) view
    }

    SavedFormDataEntryAdminIntegrationMixin <|-- SavedFormDataEntryAdmin

    FormHandlerEntryInline ..> SavedFormDataEntryAdminIntegrationMixin : uses export endpoint
    SavedFormDataEntryAdminIntegrationMixin ..> DjangoAdminSite : uses admin_view
    FormHandlerEntryInline ..> DjangoAdminSite : uses reverse admin_unfold_fobi_savedformdataentry_export
Loading

File-Level Changes

Change Details Files
Introduce a dedicated admin export endpoint on SavedFormDataEntry that delegates to export_data while enforcing view permissions and site-scoped querysets.
  • Override get_urls in SavedFormDataEntryAdminIntegrationMixin to register an export/ path named unfold_fobi_savedformdataentry_export, wrapped with admin_site.admin_view.
  • Implement export_for_form_entry admin view method that checks has_view_permission, validates the form_entry_id query parameter, builds a queryset via self.get_queryset(request).filter(form_entry_id=form_entry_id), and calls self.export_data(request, queryset).
src/unfold_fobi/admin/saved_data_entry.py
Wire the inline "Export entries" action button to the new admin export endpoint instead of the upstream Fobi export view.
  • Update FormHandlerEntryInline.handler_actions so that the "Export entries" branch builds action_url using reverse("admin:unfold_fobi_savedformdataentry_export") with ?form_entry_id=<form_entry_id> query string.
  • Keep the download icon and other inline actions unchanged.
src/unfold_fobi/admin/inlines.py
Add comprehensive regression tests for the new export endpoint, inline URL wiring, permissions, input validation, and site scoping behavior.
  • Introduce fixtures for saved entries, various user roles (admin, other staff, view-only staff, non-staff), and clients.
  • Add tests ensuring the inline button points to the new endpoint and no longer links to the upstream Fobi export URL, while preserving the existing "View entries" redirect.
  • Add tests verifying export_for_form_entry delegates to export_data with a form_entry-filtered queryset, works for non-owner staff, and handles empty forms and unrelated forms correctly.
  • Add permission tests ensuring anonymous and non-staff users are blocked while staff with view permission succeed.
  • Add input validation tests for missing and non-integer form_entry_id, expecting HTTP 400.
  • Add tests verifying the endpoint respects site scoping by using get_queryset(request), via a custom ScopedAdmin composed with RelationSiteScopeAdminMixin, asserting empty vs non-empty querysets based on site access.
  • Document the task and implementation details in a new .agents task file describing T27, its scope, security considerations, and acceptance criteria.
tests/admin/test_db_store_export_action.py
.agents/tasks/T27_fix_db_store_export_inline_button.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@metaforx
metaforx merged commit a34abdd into main May 4, 2026
4 checks passed
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.

1 participant