diff --git a/hypha/apply/activity/migrations/0082_change_staff_pii_visibility.py b/hypha/apply/activity/migrations/0082_change_staff_pii_visibility.py index d9cd3c3f06..d58e1529d3 100644 --- a/hypha/apply/activity/migrations/0082_change_staff_pii_visibility.py +++ b/hypha/apply/activity/migrations/0082_change_staff_pii_visibility.py @@ -4,7 +4,7 @@ import re from hypha.apply.activity.adapters.activity_feed import ActivityAdapter -from hypha.apply.activity.models import TEAM, Activity +from hypha.apply.activity.models import TEAM from hypha.apply.activity.options import MESSAGES @@ -27,6 +27,7 @@ def change_updatelead_visibility(apps, schema_editor): ActivityAdapter.messages[MESSAGES.UNARCHIVE_SUBMISSION], ] + Activity = apps.get_model("activity", "Activity") staff_identity_set = Activity.objects.none() for message in lead_messages: diff --git a/hypha/apply/activity/migrations/0088_activity_deleted.py b/hypha/apply/activity/migrations/0088_activity_deleted.py new file mode 100644 index 0000000000..02e95ff85c --- /dev/null +++ b/hypha/apply/activity/migrations/0088_activity_deleted.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.11 on 2026-02-18 14:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("activity", "0087_alter_event_type"), + ] + + operations = [ + migrations.AddField( + model_name="activity", + name="deleted", + field=models.DateTimeField(default=None, null=True), + ), + ] diff --git a/hypha/apply/activity/models.py b/hypha/apply/activity/models.py index 99addde1f7..c3d8cae455 100644 --- a/hypha/apply/activity/models.py +++ b/hypha/apply/activity/models.py @@ -216,6 +216,7 @@ class Activity(models.Model): # Fields for handling versioning of the comment activity models edited = models.DateTimeField(default=None, null=True) + deleted = models.DateTimeField(default=None, null=True) current = models.BooleanField(default=True) previous = models.ForeignKey("self", on_delete=models.CASCADE, null=True) diff --git a/hypha/apply/activity/services.py b/hypha/apply/activity/services.py index 7806ab4483..1eff7ae1c9 100644 --- a/hypha/apply/activity/services.py +++ b/hypha/apply/activity/services.py @@ -37,6 +37,33 @@ def edit_comment(activity: Activity, message: str) -> Activity: return activity +def delete_comment(activity: Activity) -> Activity: + """ + Soft delete a comment by creating a clone of the original comment with a delete message. + + Args: + activity (Activity): The original comment activity to be soft deleted. + + Returns: + Activity: The soft deleted comment activitye. + """ + + # Create a clone of the comment to soft delete + previous = Activity.objects.get(pk=activity.pk) + previous.pk = None + previous.current = False + previous.save() + + activity.previous = previous + activity.deleted = timezone.now() + activity.edited = None + activity.message = "" + activity.current = True + activity.save() + + return activity + + def get_related_activities_for_user(obj, user): """Return comments/communications related to an object, esp. useful with ApplicationSubmission and Project. diff --git a/hypha/apply/activity/templates/activity/partial_comment_message.html b/hypha/apply/activity/templates/activity/partial_comment_message.html index dd2e3808b7..60583b2294 100644 --- a/hypha/apply/activity/templates/activity/partial_comment_message.html +++ b/hypha/apply/activity/templates/activity/partial_comment_message.html @@ -1,11 +1,15 @@ -{% load heroicons activity_tags nh3_tags markdown_tags submission_tags apply_tags users_tags %} +{% load i18n heroicons activity_tags nh3_tags markdown_tags submission_tags apply_tags users_tags %}
{{ activity|display_for:request.user|submission_links|markdown|nh3 }}
{% if activity.edited %} - (edited) + ({% trans "edited" %}) +{% endif %} + +{% if activity.deleted %} + ({% trans "deleted" %}) {% endif %} {% with activity.attachments.all as attachments %} diff --git a/hypha/apply/activity/templates/activity/ui/activity-comment-item.html b/hypha/apply/activity/templates/activity/ui/activity-comment-item.html index 5670004f4d..dc35b2c6d4 100644 --- a/hypha/apply/activity/templates/activity/ui/activity-comment-item.html +++ b/hypha/apply/activity/templates/activity/ui/activity-comment-item.html @@ -1,6 +1,6 @@ {% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags heroicons users_tags %} -
+
@@ -46,7 +46,6 @@
{% if not request.user.is_applicant %} {% if request.user.is_apply_staff and activity.assigned_to %} - {% heroicon_outline "user-plus" size=14 class="inline" aria_hidden=true %} {% if activity.assigned_to.id == request.user.id %} @@ -64,20 +63,32 @@ {{ visibility_text }} {% endwith %} - {% endif %} - {% if editable and activity.user == request.user %} + {% if editable and activity.user == request.user and not activity.deleted %} {% heroicon_micro "pencil-square" aria_hidden=true %} {% trans "Edit" %} {% endif %} + + {% if editable and activity.user == request.user and not activity.deleted and request.user.is_apply_staff %} + + {% heroicon_micro "trash" class="opacity-80 size-4" aria_hidden=true %} + {% trans "Delete" %} + + {% endif %}
diff --git a/hypha/apply/activity/urls.py b/hypha/apply/activity/urls.py index ea59d0258d..bd26613df1 100644 --- a/hypha/apply/activity/urls.py +++ b/hypha/apply/activity/urls.py @@ -1,6 +1,12 @@ from django.urls import include, path -from .views import AttachmentView, NotificationsView, edit_comment, partial_comments +from .views import ( + AttachmentView, + NotificationsView, + delete_comment, + edit_comment, + partial_comments, +) app_name = "activity" @@ -10,6 +16,7 @@ path("notifications/", NotificationsView.as_view(), name="notifications"), path("comments//", partial_comments, name="partial-comments"), path("/edit-comment/", edit_comment, name="edit-comment"), + path("/delete-comment/", delete_comment, name="delete-comment"), path( "activities/attachment//download/", AttachmentView.as_view(), diff --git a/hypha/apply/activity/views.py b/hypha/apply/activity/views.py index 449d2b9bb7..506700a119 100644 --- a/hypha/apply/activity/views.py +++ b/hypha/apply/activity/views.py @@ -1,4 +1,4 @@ -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, user_passes_test from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator from django.shortcuts import get_object_or_404, render @@ -8,7 +8,7 @@ from rolepermissions.checkers import has_object_permission from hypha.apply.funds.models.submissions import ApplicationSubmission -from hypha.apply.users.decorators import staff_required +from hypha.apply.users.decorators import is_apply_staff, staff_required from hypha.apply.utils.storage import PrivateMediaView from . import services @@ -59,6 +59,9 @@ def edit_comment(request, pk): if activity.type != COMMENT or activity.user != request.user: raise PermissionError("You can only edit your own comments") + if activity.deleted: + raise PermissionError("You can not edit a deleted comment") + if request.GET.get("action") == "cancel": return render( request, @@ -78,6 +81,34 @@ def edit_comment(request, pk): return render(request, "activity/ui/edit_comment_form.html", {"activity": activity}) +@login_required +@user_passes_test(is_apply_staff) +def delete_comment(request, pk): + """Soft delete a comment.""" + activity = get_object_or_404(Activity, id=pk) + + if activity.type != COMMENT or activity.user != request.user: + raise PermissionError("You can only delete your own comments") + + if activity.deleted: + raise PermissionError("You can not delete a deleted comment") + + if request.method == "DELETE": + activity = services.delete_comment(activity) + + return render( + request, + "activity/ui/activity-comment-item.html", + {"activity": activity, "success": True}, + ) + + return render( + request, + "activity/ui/activity-comment-item.html", + {"activity": activity}, + ) + + class ActivityContextMixin: """Mixin to add related 'comments' of the current view's 'self.object'"""