Skip to content

feat(posts): add safe delete flow for custom post types#247

Merged
gregagi merged 2 commits intomainfrom
forge/custom-post-type-delete-flow
Mar 19, 2026
Merged

feat(posts): add safe delete flow for custom post types#247
gregagi merged 2 commits intomainfrom
forge/custom-post-type-delete-flow

Conversation

@gregagi
Copy link
Collaborator

@gregagi gregagi commented Mar 19, 2026

Summary\n- add explicit confirmation flow for custom post type deletion via in-page modal\n- require backend delete confirmation and block deletion when active (non-archived) ideas still reference the type\n- keep clear user messaging for unconfirmed and blocked delete attempts\n- add regression tests for confirmed delete and blocked-delete scenarios\n- update CHANGELOG with the shipped delete flow\n\n## Testing\n- local pytest execution in this runtime is blocked because PostgreSQL host db is unavailable outside docker compose\n- CI will run the full suite

Summary by CodeRabbit

  • New Features
    • Custom post type deletion now requires explicit confirmation through a modal dialog with an acknowledgement checkbox
    • Deletion is prevented when the post type is referenced by active ideas; users are instructed to archive those ideas first

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR enhances the custom post type deletion flow by introducing an explicit modal confirmation dialog and backend dependency validation. The backend now prevents deletion when active ideas reference the custom post type, with corresponding test coverage for both successful and blocked deletion scenarios.

Changes

Cohort / File(s) Summary
Changelog & Documentation
CHANGELOG.md
Added entry documenting the new safe custom post type deletion flow with explicit confirmation modal and backend dependency guardrails.
Backend View Logic
core/views.py
Enhanced ProjectCustomPostTypeDeleteView.post to require explicit confirm_delete=yes confirmation and added a dependency check that blocks deletion when non-archived title_suggestions reference the post type.
Backend Tests
core/tests/test_custom_post_types.py
Added three comprehensive test cases covering: deletion without confirmation, deletion blocked by active ideas, and successful deletion of unused post types.
Frontend Controller
frontend/src/controllers/custom_post_type_delete_modal_controller.js
New Stimulus controller managing modal dialog lifecycle with handlers for opening dialogs, closing on user action, and closing on backdrop click.
Frontend Template
frontend/templates/project/project_custom_post_types.html
Replaced inline confirm() deletion with a modal-based flow; delete button now triggers modal dialog containing the POST form with hidden confirm_delete=yes field and required checkbox acknowledgement.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend as Frontend UI
    participant Modal
    participant Backend as Backend View
    participant Database

    User->>Frontend: Click Delete button
    Frontend->>Modal: Trigger open action
    Modal->>Modal: Display modal dialog
    User->>Modal: Check acknowledgement + Click Confirm
    Modal->>Backend: POST with confirm_delete=yes
    
    Backend->>Backend: Check confirmation flag
    alt Confirmation Valid
        Backend->>Database: Query active title_suggestions
        alt Dependencies Found
            Backend->>Backend: Build error message
            Backend->>Frontend: Redirect with error
            Frontend->>User: Display "Cannot delete - active ideas"
        else No Dependencies
            Backend->>Database: Delete custom post type
            Backend->>Frontend: Redirect to success page
            Frontend->>User: Deletion confirmed
        end
    else Confirmation Missing
        Backend->>Frontend: Redirect with error
        Frontend->>User: Display "Delete not confirmed"
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly Related PRs

  • PR #220: Initial custom post type delete functionality that this PR extends with safety mechanisms, confirmation flow, and dependency validation.

Poem

🐰 A deletion so cautious, with modals so clear,
Confirming each choice with a careful peer,
Dependencies checked, no ideas left behind,
A safer way forward for custom posts to unwind! 🗑️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main change: adding a safe deletion flow for custom post types with confirmation and dependency checks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch forge/custom-post-type-delete-flow
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR introduces a safe-delete flow for custom post types, replacing the previous browser confirm() dialog with a proper in-page <dialog> modal that includes an explicit confirmation checkbox, and adding a backend guard that blocks deletion while any active (non-archived) ideas still reference the post type.

Key changes:

  • ProjectCustomPostTypeDeleteView now requires confirm_delete=yes in the POST body and rejects the request with a user-friendly error message if it is absent or if active ideas reference the type
  • The HTML template renders a per-post-type <dialog> element with a unique ID, opened by a data-delete-modal-open trigger button and closed via a data-delete-modal-close cancel button
  • Three regression tests cover: missing confirmation, blocked deletion due to active ideas, and successful deletion
  • The new JavaScript for wiring up the modal is written as a raw inline <script> block rather than a Stimulus controller, which violates the project's frontend.mdc convention ("Prefer Stimulus JS for adding interactivity to Django templates instead of raw script elements")

Confidence Score: 4/5

  • Safe to merge with minor style clean-up; backend logic is correct and well-tested.
  • The backend guard logic is sound, ownership checks are enforced via get_object_or_404, three targeted regression tests cover the critical paths, and the CSRF token is present in the form. The only actionable issues are a convention violation (raw <script> vs Stimulus) and a UX gap (no backdrop-click-to-close on the dialog). Neither is a runtime defect, but the Stimulus convention violation is a direct conflict with the project's declared frontend rules.
  • frontend/templates/project/project_custom_post_types.html — raw script block should become a Stimulus controller per project conventions.

Important Files Changed

Filename Overview
core/views.py Added confirm_delete=yes guard and active-dependency check to ProjectCustomPostTypeDeleteView.post(); logic is correct and ownership is properly enforced; the only nuance is that archived ideas referencing the type are silently nulled on delete (by SET_NULL), which is intentional but worth documenting.
frontend/templates/project/project_custom_post_types.html Replaced the inline confirm() dialog with a proper <dialog> modal per-post-type inside the loop; functional and accessible, but uses a raw <script> block rather than a Stimulus controller (violates frontend.mdc), and the backdrop click does not dismiss the dialog.
core/tests/test_custom_post_types.py Three new regression tests cover the unconfirmed-delete path, the blocked-delete-with-active-ideas path, and the happy-path successful deletion; all look correct and use proper Django test fixtures.
CHANGELOG.md Changelog entry accurately describes the safe delete flow addition; no issues.

Sequence Diagram

sequenceDiagram
    actor User
    participant Browser
    participant Django as Django (ProjectCustomPostTypeDeleteView)
    participant DB

    User->>Browser: Clicks "Delete" button
    Browser->>Browser: showModal() → <dialog> opens
    User->>Browser: Checks confirmation checkbox & clicks "Delete post type"
    Browser->>Django: POST /project/{pk}/post-type/{post_type_pk}/delete/<br/>confirm_delete=yes
    Django->>Django: Check confirm_delete == "yes"
    alt confirm_delete missing / not "yes"
        Django-->>Browser: redirect → error message "Delete not confirmed"
    else confirmed
        Django->>DB: COUNT title_suggestions WHERE archived=False
        alt active_dependency_count > 0
            Django-->>Browser: redirect → error message "Cannot delete '…'"
        else no active dependencies
            Django->>DB: DELETE ProjectCustomPostType
            DB-->>Django: SET_NULL on archived idea FKs
            Django-->>Browser: redirect → success message "Deleted '…'"
        end
    end
Loading

Last reviewed commit: "feat(posts): add saf..."

Comment on lines +109 to +128
<script>
document.querySelectorAll("[data-delete-modal-open]").forEach((button) => {
button.addEventListener("click", () => {
const modalId = button.getAttribute("data-delete-modal-open");
const modal = document.getElementById(modalId);
if (modal) {
modal.showModal();
}
});
});

document.querySelectorAll("[data-delete-modal-close]").forEach((button) => {
button.addEventListener("click", () => {
const modal = button.closest("dialog");
if (modal) {
modal.close();
}
});
});
</script>
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Raw <script> instead of Stimulus controller

The project's frontend.mdc rule explicitly states: "Prefer Stimulus JS for adding interactivity to Django templates instead of raw script elements" and "New controllers should be created in frontend/src/controllers directory".

This inline <script> block that wires up the modal open/close behavior should instead be a dedicated Stimulus controller (e.g., delete-modal_controller.js) using data-controller, data-action, and target attributes. The current script tag circumvents the established convention for encapsulating JS behaviour.

Rule Used: .cursor/rules/frontend.mdc (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +72 to +99
<dialog id="delete-post-type-modal-{{ post_type.id }}" class="p-0 w-full max-w-md rounded-lg border border-gray-200 shadow-xl backdrop:bg-black/50">
<div class="p-6 space-y-4">
<div>
<h4 class="text-lg font-semibold text-gray-900">Delete "{{ post_type.name }}"?</h4>
<p class="mt-2 text-sm text-gray-600">
This action cannot be undone. Deletion is blocked if this type is still used by active ideas.
</p>
</div>

<form method="post" action="{% url 'project_custom_post_type_delete' project.id post_type.id %}" class="space-y-4">
{% csrf_token %}
<input type="hidden" name="confirm_delete" value="yes" />
<label class="flex gap-2 items-start text-sm text-gray-700">
<input type="checkbox" required class="mt-0.5 rounded border-gray-300" />
<span>I understand this will permanently delete this custom post type if it is safe to do so.</span>
</label>

<div class="flex gap-3 justify-end items-center">
<button type="button" class="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white rounded-md border border-gray-300 hover:bg-gray-50" data-delete-modal-close>
Cancel
</button>
<button type="submit" class="inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-red-600 rounded-md border border-red-600 hover:bg-red-700">
Delete post type
</button>
</div>
</form>
</div>
</dialog>
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Dialog backdrop click does not close the modal

The <dialog> element opened with showModal() does not close when the user clicks on the semi-transparent backdrop outside the dialog box. The native <dialog> element fires a cancel event when Escape is pressed, but there is no built-in backdrop-click-to-close behaviour. Users who click outside expecting the dialog to dismiss will be confused.

Consider adding a click listener on the <dialog> element itself (the backdrop area outside .p-6 triggers the event on the dialog, not on inner content):

modal.addEventListener("click", (event) => {
  if (event.target === modal) {
    modal.close();
  }
});

Or, if the intent is to intentionally prevent accidental dismissal for this destructive action, a brief comment would clarify that this is deliberate.

messages.error(request, "Delete not confirmed. Custom post type was not deleted.")
return redirect("project_custom_post_types", pk=project.pk)

active_dependency_count = custom_post_type.title_suggestions.filter(archived=False).count()
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Only active ideas are checked; archived ideas with the post type may be silently nulled

The guard only prevents deletion while archived=False ideas reference the type. If a post type has only archived ideas, deletion proceeds and those archived ideas will have their custom_post_type FK silently set to NULL (via on_delete=models.SET_NULL). This is likely the intended behaviour given the PR description, but the user-facing modal copy says "Deletion is blocked if this type is still used by active ideas" — which matches. Just worth confirming this silent nullification of archived idea references is intentional and expected downstream (e.g., in analytics or listing views that may previously filter by post type).

@gregagi
Copy link
Collaborator Author

gregagi commented Mar 19, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gregagi gregagi merged commit a1d1595 into main Mar 19, 2026
3 of 4 checks passed
@gregagi gregagi deleted the forge/custom-post-type-delete-flow branch March 19, 2026 06:10
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