Skip to content

Commit 074576a

Browse files
authored
Allow staff to delete their own comments. (#4705)
Fixes #4606
1 parent 7220033 commit 074576a

8 files changed

Lines changed: 110 additions & 11 deletions

File tree

hypha/apply/activity/migrations/0082_change_staff_pii_visibility.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import re
55

66
from hypha.apply.activity.adapters.activity_feed import ActivityAdapter
7-
from hypha.apply.activity.models import TEAM, Activity
7+
from hypha.apply.activity.models import TEAM
88
from hypha.apply.activity.options import MESSAGES
99

1010

@@ -27,6 +27,7 @@ def change_updatelead_visibility(apps, schema_editor):
2727
ActivityAdapter.messages[MESSAGES.UNARCHIVE_SUBMISSION],
2828
]
2929

30+
Activity = apps.get_model("activity", "Activity")
3031
staff_identity_set = Activity.objects.none()
3132

3233
for message in lead_messages:
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.2.11 on 2026-02-18 14:46
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("activity", "0087_alter_event_type"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="activity",
14+
name="deleted",
15+
field=models.DateTimeField(default=None, null=True),
16+
),
17+
]

hypha/apply/activity/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ class Activity(models.Model):
216216

217217
# Fields for handling versioning of the comment activity models
218218
edited = models.DateTimeField(default=None, null=True)
219+
deleted = models.DateTimeField(default=None, null=True)
219220
current = models.BooleanField(default=True)
220221
previous = models.ForeignKey("self", on_delete=models.CASCADE, null=True)
221222

hypha/apply/activity/services.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,33 @@ def edit_comment(activity: Activity, message: str) -> Activity:
3737
return activity
3838

3939

40+
def delete_comment(activity: Activity) -> Activity:
41+
"""
42+
Soft delete a comment by creating a clone of the original comment with a delete message.
43+
44+
Args:
45+
activity (Activity): The original comment activity to be soft deleted.
46+
47+
Returns:
48+
Activity: The soft deleted comment activitye.
49+
"""
50+
51+
# Create a clone of the comment to soft delete
52+
previous = Activity.objects.get(pk=activity.pk)
53+
previous.pk = None
54+
previous.current = False
55+
previous.save()
56+
57+
activity.previous = previous
58+
activity.deleted = timezone.now()
59+
activity.edited = None
60+
activity.message = ""
61+
activity.current = True
62+
activity.save()
63+
64+
return activity
65+
66+
4067
def get_related_activities_for_user(obj, user):
4168
"""Return comments/communications related to an object, esp. useful with
4269
ApplicationSubmission and Project.

hypha/apply/activity/templates/activity/partial_comment_message.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
{% load heroicons activity_tags nh3_tags markdown_tags submission_tags apply_tags users_tags %}
1+
{% load i18n heroicons activity_tags nh3_tags markdown_tags submission_tags apply_tags users_tags %}
22

33
<div class="max-w-none prose">
44
{{ activity|display_for:request.user|submission_links|markdown|nh3 }}
55
</div>
66

77
{% if activity.edited %}
8-
<span class="text-sm text-fg-muted" data-tippy-content="{{ activity.edited|date:"SHORT_DATETIME_FORMAT" }}">(edited)</span>
8+
<span class="text-sm text-fg-muted" data-tippy-content="{{ activity.edited|date:"SHORT_DATETIME_FORMAT" }}">({% trans "edited" %})</span>
9+
{% endif %}
10+
11+
{% if activity.deleted %}
12+
<span class="text-sm text-fg-muted" data-tippy-content="{{ activity.deleted|date:"SHORT_DATETIME_FORMAT" }}">({% trans "deleted" %})</span>
913
{% endif %}
1014

1115
{% with activity.attachments.all as attachments %}

hypha/apply/activity/templates/activity/ui/activity-comment-item.html

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags heroicons users_tags %}
22

3-
<article class="relative h-timeline-item">
3+
<article class="relative h-timeline-item" id="communications-wrapper--{{ activity.id }}">
44
<div
55
class="flex py-3 -ms-8 before:block before:absolute before:top-0 before:bottom-0 before:left-0 before:w-0.5 before:bg-base-300 md:-ms-20"
66
>
@@ -46,7 +46,6 @@
4646
<div class="flex gap-1 items-center">
4747
{% if not request.user.is_applicant %}
4848
{% if request.user.is_apply_staff and activity.assigned_to %}
49-
5049
<span class="flex gap-1 items-center py-0.5 px-1.5 text-xs rounded-xl border border-gray-300 text-fg-muted">
5150
{% heroicon_outline "user-plus" size=14 class="inline" aria_hidden=true %}
5251
{% if activity.assigned_to.id == request.user.id %}
@@ -64,20 +63,32 @@
6463
{{ visibility_text }}
6564
</span>
6665
{% endwith %}
67-
6866
{% endif %}
6967

70-
{% if editable and activity.user == request.user %}
68+
{% if editable and activity.user == request.user and not activity.deleted %}
7169
<a
7270
hx-get="{% url 'activity:edit-comment' activity.id %}"
7371
hx-target="#text-comment-{{activity.id}}"
74-
title="Edit Comment"
72+
title="{% trans "Edit comment" %}"
7573
class="btn btn-sm btn-square btn-ghost"
7674
>
7775
{% heroicon_micro "pencil-square" aria_hidden=true %}
7876
<span class="sr-only">{% trans "Edit" %}</span>
7977
</a>
8078
{% endif %}
79+
80+
{% if editable and activity.user == request.user and not activity.deleted and request.user.is_apply_staff %}
81+
<a
82+
hx-delete="{% url 'activity:delete-comment' activity.id %}"
83+
hx-target="#communications-wrapper--{{activity.id}}"
84+
hx-confirm="{% trans "Are you sure you want to delete this comment? This action cannot be undone." %}"
85+
title="{% trans "Delete comment" %}"
86+
class="btn btn-error btn-sm btn-square btn-ghost"
87+
>
88+
{% heroicon_micro "trash" class="opacity-80 size-4" aria_hidden=true %}
89+
<span class="sr-only">{% trans "Delete" %}</span>
90+
</a>
91+
{% endif %}
8192
</div>
8293
</header>
8394

hypha/apply/activity/urls.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
from django.urls import include, path
22

3-
from .views import AttachmentView, NotificationsView, edit_comment, partial_comments
3+
from .views import (
4+
AttachmentView,
5+
NotificationsView,
6+
delete_comment,
7+
edit_comment,
8+
partial_comments,
9+
)
410

511
app_name = "activity"
612

@@ -10,6 +16,7 @@
1016
path("notifications/", NotificationsView.as_view(), name="notifications"),
1117
path("comments/<int:pk>/", partial_comments, name="partial-comments"),
1218
path("<pk>/edit-comment/", edit_comment, name="edit-comment"),
19+
path("<pk>/delete-comment/", delete_comment, name="delete-comment"),
1320
path(
1421
"activities/attachment/<uuid:file_pk>/download/",
1522
AttachmentView.as_view(),

hypha/apply/activity/views.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.contrib.auth.decorators import login_required
1+
from django.contrib.auth.decorators import login_required, user_passes_test
22
from django.core.exceptions import PermissionDenied
33
from django.core.paginator import Paginator
44
from django.shortcuts import get_object_or_404, render
@@ -8,7 +8,7 @@
88
from rolepermissions.checkers import has_object_permission
99

1010
from hypha.apply.funds.models.submissions import ApplicationSubmission
11-
from hypha.apply.users.decorators import staff_required
11+
from hypha.apply.users.decorators import is_apply_staff, staff_required
1212
from hypha.apply.utils.storage import PrivateMediaView
1313

1414
from . import services
@@ -59,6 +59,9 @@ def edit_comment(request, pk):
5959
if activity.type != COMMENT or activity.user != request.user:
6060
raise PermissionError("You can only edit your own comments")
6161

62+
if activity.deleted:
63+
raise PermissionError("You can not edit a deleted comment")
64+
6265
if request.GET.get("action") == "cancel":
6366
return render(
6467
request,
@@ -78,6 +81,34 @@ def edit_comment(request, pk):
7881
return render(request, "activity/ui/edit_comment_form.html", {"activity": activity})
7982

8083

84+
@login_required
85+
@user_passes_test(is_apply_staff)
86+
def delete_comment(request, pk):
87+
"""Soft delete a comment."""
88+
activity = get_object_or_404(Activity, id=pk)
89+
90+
if activity.type != COMMENT or activity.user != request.user:
91+
raise PermissionError("You can only delete your own comments")
92+
93+
if activity.deleted:
94+
raise PermissionError("You can not delete a deleted comment")
95+
96+
if request.method == "DELETE":
97+
activity = services.delete_comment(activity)
98+
99+
return render(
100+
request,
101+
"activity/ui/activity-comment-item.html",
102+
{"activity": activity, "success": True},
103+
)
104+
105+
return render(
106+
request,
107+
"activity/ui/activity-comment-item.html",
108+
{"activity": activity},
109+
)
110+
111+
81112
class ActivityContextMixin:
82113
"""Mixin to add related 'comments' of the current view's 'self.object'"""
83114

0 commit comments

Comments
 (0)