Skip to content

feat: allow users to delete their own comments (#2083)#4975

Merged
hlbmtc merged 5 commits into
mainfrom
feat/self-comment-delete
Jul 6, 2026
Merged

feat: allow users to delete their own comments (#2083)#4975
hlbmtc merged 5 commits into
mainfrom
feat/self-comment-delete

Conversation

@aseckin

@aseckin aseckin commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets users soft-delete their own comments without pestering a moderator, and routes all deletions (author and staff) through a confirmation modal. Addresses the delete half of #2083.

Previously the "Delete" action existed only for staff and deleted instantly with no confirmation.

Scope note

Issue #2083 also requested a public ↔ private toggle. That half is intentionally not included here: private comments are deprecated (replaced by "Private Notes") and are filtered out of the feed for everyone — including the author — with no UI to view or restore them, so a naive "Make private" would make a comment vanish even for its author. We shipped delete-only by decision; the private/public toggle can be revisited separately. Please don't auto-close #2083 as fully resolved.

Changes

Backendcomments/views/common.py

  • comment_delete_api_view changed from IsAdminUser to IsAuthenticated + an author-or-staff check (returns 403 otherwise).
  • Reuses the existing soft_delete_comment service, so threads with replies keep their "deleted" placeholder and comment counts stay correct.

Frontendfront_end/src/components/comment_feed/comment.tsx

  • The "Delete" menu item is now shown to the comment author (in addition to staff).
  • Clicking "Delete" (for author or staff) opens a ConfirmModal instead of deleting immediately.

i18ndeleteCommentConfirmTitle / deleteCommentConfirmDescription added to all six message files (en, es, cs, pt, zh, zh-TW).

Tests

New TestCommentDelete in tests/unit/test_comments/test_views.py: author → 200 (soft-deleted); non-author non-staff → 403; staff → 200; anonymous → 401/403.

Verification

  • pytest tests/unit/test_comments/test_views.py → 32 passed
  • ruff format / ruff check → clean
  • bun run lint → 0 errors; tsc → 0; bun run build → success

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a delete-confirmation dialog when removing comments.
    • Updated comment deletion permissions to allow comment authors and staff to delete comments (instead of admin-only).
  • Bug Fixes
    • Reduced accidental deletions by switching to a confirmation-based delete flow.
  • Documentation
    • Added comment-deletion confirmation title and description text to multiple languages.
  • Tests
    • Added unit tests covering author, staff, non-author, and anonymous delete behaviors.

Previously only staff could soft-delete comments, and deletion happened
instantly with no confirmation. Authors can now delete their own comments,
and all deletions (author and staff) go through a confirmation modal.

Backend: the delete endpoint now permits the comment author in addition to
staff. Frontend: the "Delete" menu item is shown to the author, and clicking
it (for author or staff) opens a confirmation modal instead of deleting
immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6138a9d-3957-4d2a-8f5e-013271cdf077

📥 Commits

Reviewing files that changed from the base of the PR and between 0420f88 and bfe7996.

📒 Files selected for processing (1)
  • front_end/src/components/comment_feed/comment.tsx

📝 Walkthrough

Walkthrough

Comment deletion now allows the comment author or staff on the backend, and the frontend shows a confirmation modal before soft-deleting a comment. New confirmation strings were added for six locales, and endpoint tests cover the updated authorization rules.

Changes

Comment delete authorization and confirmation flow

Layer / File(s) Summary
Author-or-staff delete authorization
comments/views/common.py, tests/unit/test_comments/test_views.py
Removes the admin-only permission decorator, adds an inline author-or-staff check for comment deletion, and adds tests for author, non-author, staff, and anonymous outcomes.
Delete confirmation modal and strings
front_end/src/components/comment_feed/comment.tsx, front_end/messages/en.json, front_end/messages/cs.json, front_end/messages/es.json, front_end/messages/pt.json, front_end/messages/zh-TW.json, front_end/messages/zh.json
Opens a confirmation modal before deleting, runs the soft-delete on confirm, updates delete visibility to author-or-staff, and adds localized confirmation text across the supported message files.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: lsabor, ncarazon

Poem

A bunny clicked, “Delete?” with care,
The modal popped up in the air.
Authors may hop, staff may too,
Soft-delete done with a tidy clue.
🐇✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR delivers self-comment deletion, but it does not implement the requested public-to-private comment editing from #2083. Add the public/private comment edit flow requested in #2083, or update the issue scope if that work is deferred.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: letting users delete their own comments.
Out of Scope Changes check ✅ Passed The modal, translations, permission update, and tests all support the comment-delete flow and appear in scope.
✨ 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 feat/self-comment-delete

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.

@aseckin aseckin requested review from hlbmtc and ncarazon July 1, 2026 10:59
@aseckin aseckin marked this pull request as ready for review July 1, 2026 10:59
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Cleanup: Preview Environment Removed

The preview environment for this PR has been destroyed.

Resource Status
🌐 Preview App Deleted
🗄️ PostgreSQL Branch Deleted
⚡ Redis Database Deleted
🔧 GitHub Deployments Removed
📦 Docker Image Retained (auto-cleanup via GHCR policies)

Cleanup triggered by PR close at 2026-07-06T16:26:40Z

@hlbmtc hlbmtc 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.

Comment thread comments/views/common.py Outdated

@api_view(["POST"])
@permission_classes([IsAdminUser])
@permission_classes([IsAuthenticated])

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.

We can remove this -- IsAuthenticated is default DRF behaviour now

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.

@claude remove @permission_classes decorator

@claude claude Bot Jul 2, 2026

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.

Claude finished @hlbmtc's task in 42s —— View job


Remove @permission_classes decorator

  • Read comments/views/common.py
  • Verify DRF default permission behavior — confirmed DEFAULT_PERMISSION_CLASSES = [IsAuthenticated] in metaculus_web/settings.py:158-160
  • Remove @permission_classes([IsAuthenticated]) from comment_delete_api_view
  • Commit change (0420f88)

Dropped the redundant @permission_classes([IsAuthenticated]) on comment_delete_api_view — the project's REST_FRAMEWORK config already applies IsAuthenticated as the default, so it was a no-op. Import left untouched since IsAuthenticated is still referenced by comment_edit_api_view and comment_toggle_cmm_view (out of scope for this ask).
• Branch: feat/self-comment-delete

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hlbmtc's task in 2m 19s —— View job


Claude PR Review

  • Read repo CLAUDE.md for conventions (no CLAUDE.md at root)
  • Read backend changes in comments/views/common.py
  • Read frontend changes in comment.tsx
  • Read i18n changes (6 locales)
  • Read new tests in test_views.py
  • Post review

Overall this looks solid. Small, well-scoped change, tests cover the four important auth cases, and the scope note in the PR description about deliberately not shipping the private/public toggle is a good call given the "Private Notes" replacement. A few notes below — nothing blocking.

Correctness / Security

comments/views/common.py:114-124 — permission check is correct.
Uses author_id != request.user.id and not request.user.is_staff before soft_delete_comment(comment). Because IsAuthenticated gates the view, request.user.id is a real int, so a comment with author_id=None (should be impossible but guards defensively) correctly fails closed. soft_delete_comment is idempotent (early-returns when already deleted), so repeated calls or races are safe.

Tests are appropriately thorough.
TestCommentDelete covers author (200), non-author (403), staff (200), anonymous (401/403). Nice.

One tiny nit on test_staff_can_delete_any_comment: the test explicitly does user_admin.is_staff = True; user_admin.save(...) — depending on how the user_admin fixture is defined, this may be redundant. Not a bug, just noise.

UX / Design considerations (non-blocking)

front_end/src/components/comment_feed/comment.tsx:641-649 — silent failure on delete.

const handleDeleteComment = async () => {
  const response = await softDeleteComment(comment.id);
  if (response && "errors" in response) {
    console.error("Error deleting comment:", response.errors);
  } else {
    setIsDeleted(true);
  }
};

ConfirmModal.handleConfirm fires onConfirm() and then immediately calls onCloseModal without awaiting. If the network call errors (e.g. a stale session, backend 403, transient 500), the modal closes and the user is silently left with the comment still there — no toast, no inline error. This mirrors the existing deleteUser handler above, so it's not a regression, but for a user-visible action worth surfacing something (a toast or a setErrorMessage). Worth cleaning up in a follow-up along with the other deleteUser handler.

comment.tsx:697 — bot-owned comments.
edit is hidden when !!user?.is_bot. delete is not gated the same way — if a bot account somehow reached this UI, it could delete comments. Almost certainly a non-issue in practice, but the inconsistency is worth a one-token fix if you want symmetry:

hidden: !(isCommentAuthor || user?.is_staff) || !!user?.is_bot,

comment.tsx:697 — pinned comments.
An author can now delete a comment even when staff has pinned it (is_pinned). If your convention is that pins are staff-curated "featured" content, you may want:

hidden: !(isCommentAuthor || user?.is_staff) || (isCommentAuthor && comment.is_pinned && !user?.is_staff),

Fine to punt — depends on product intent.

English copy — deleteCommentConfirmDescription.
"This can't be undone." — technically it's a soft-delete that staff can reverse, but this is user-facing and staff-undo isn't exposed to end users, so the framing is fine. Just flagging in case product wants "This can't be undone by you." or similar.

Positives

  • Reuses existing soft_delete_comment service so reply-threads and post.update_comment_count() / snapshot decrements Just Work.
  • Confirmation modal is a real correctness win over the prior instant-delete behavior for staff.
  • i18n coverage is complete across all six locales.
  • The PR description's scope note on #2083 is exactly right — don't auto-close.

• Branch: feat/self-comment-delete

DRF's DEFAULT_PERMISSION_CLASSES already enforces IsAuthenticated
project-wide (metaculus_web/settings.py), so the explicit decorator
on comment_delete_api_view was a no-op.

Co-authored-by: Hlib <hlbmtc@users.noreply.github.com>
@claude claude Bot temporarily deployed to testing_env July 2, 2026 12:45 Inactive
@claude claude Bot temporarily deployed to testing_env July 2, 2026 12:45 Inactive

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/src/components/comment_feed/comment.tsx`:
- Around line 641-650: The delete flow in
handleDeleteComment/ConfirmModal.handleConfirm closes the modal before
softDeleteComment finishes and hides failures from the user. Make
ConfirmModal.handleConfirm await the onConfirm callback before calling
onCloseModal(isOpen), and update handleDeleteComment to surface any delete error
with the existing toast pattern instead of only console.error so the user gets
clear failure feedback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dc50b4ce-8254-4909-b2ad-0187609444d8

📥 Commits

Reviewing files that changed from the base of the PR and between dfa9a2f and 0420f88.

📒 Files selected for processing (9)
  • comments/views/common.py
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/components/comment_feed/comment.tsx
  • tests/unit/test_comments/test_views.py

Comment on lines +641 to +650
const handleDeleteComment = async () => {
const response = await softDeleteComment(comment.id);

if (response && "errors" in response) {
console.error("Error deleting comment:", response.errors);
} else {
setIsDeleted(true);
}
};

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Delete errors are silently swallowed; modal closes before the request even resolves.

ConfirmModal.handleConfirm calls onConfirm() and immediately calls onCloseModal(isOpen) without awaiting it: const handleConfirm = () => { onConfirm(); onCloseModal(isOpen); }; Since handleDeleteComment is async and only passed here, the modal closes instantly regardless of whether softDeleteComment succeeds or fails. On failure, the only feedback is console.error("Error deleting comment:", response.errors) — the user sees the confirmation dialog disappear with no indication the delete failed, and the comment silently remains.

The file already uses toast(...) for similar situations (e.g. the predictors-mention warning), so surfacing the error the same way would be a small, high-value fix.

🐛 Proposed fix: surface delete failures to the user
   const handleDeleteComment = async () => {
     const response = await softDeleteComment(comment.id);

     if (response && "errors" in response) {
       console.error("Error deleting comment:", response.errors);
+      toast(t("commentDeleteError"));
     } else {
       setIsDeleted(true);
     }
   };

Also applies to: 1183-1190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/components/comment_feed/comment.tsx` around lines 641 - 650,
The delete flow in handleDeleteComment/ConfirmModal.handleConfirm closes the
modal before softDeleteComment finishes and hides failures from the user. Make
ConfirmModal.handleConfirm await the onConfirm callback before calling
onCloseModal(isOpen), and update handleDeleteComment to surface any delete error
with the existing toast pattern instead of only console.error so the user gets
clear failure feedback.

setIsDeleted(true);
}
},
onClick: () => setIsDeleteModalOpen(true),

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.

@claude Let's reuse global ConfirmModal which could be triggered via global modal context

@claude claude Bot Jul 6, 2026

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.

Claude finished @hlbmtc's task in 3m 36s —— View job


Reuse global ConfirmModal via global modal context

  • Find the global modal context and identify the API (useModal in contexts/modal_context.tsx exposes a "confirm" ModalType)
  • Wire the delete confirmation through it in comment.tsx
  • Drop the now-redundant local ConfirmModal import, isDeleteModalOpen state, and inline <ConfirmModal /> render
  • Verify no other ConfirmModal behavior in this component is affected
  • Lint / typecheck clean
  • Commit (bfe7996)

What changed

front_end/src/components/comment_feed/comment.tsx:

  • Added useModal from @/contexts/modal_context, dropped local ConfirmModal import.

  • Removed the isDeleteModalOpen useState — the global provider owns modal state now.

  • Removed the trailing <ConfirmModal ... /> render.

  • The "delete" menu item now opens the confirm modal via context, matching the pattern already used in key_factors/hooks.ts:463 (useKeyFactorDelete):

    onClick: () =>
      setCurrentModal({
        type: "confirm",
        data: {
          title: t("deleteCommentConfirmTitle"),
          description: t("deleteCommentConfirmDescription"),
          actionText: t("delete"),
          onConfirm: handleDeleteComment,
        },
      }),

The i18n keys (deleteCommentConfirmTitle / deleteCommentConfirmDescription) and the handleDeleteComment handler are unchanged — only the wiring moved.

Notes / scope

  • Left handleDeleteComment's error handling alone (still console.error on failure). CodeRabbit's toast/await-error suggestion is orthogonal to this refactor and applies equally to the sibling deleteUser handler — worth a follow-up covering both, but out of scope for this ask.

• Branch: feat/self-comment-delete

Co-authored-by: Hlib <hlbmtc@users.noreply.github.com>
@claude claude Bot temporarily deployed to testing_env July 6, 2026 16:22 Inactive
@claude claude Bot temporarily deployed to testing_env July 6, 2026 16:22 Inactive
@hlbmtc hlbmtc merged commit 94edeb6 into main Jul 6, 2026
11 of 13 checks passed
@hlbmtc hlbmtc deleted the feat/self-comment-delete branch July 6, 2026 16:26
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.

Self-comment delete and editing "private" <> "public"

3 participants