From fb112bf0d48e62c9d1d1776ce9e731cea85deddc Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Tue, 17 Feb 2026 11:23:24 -0500 Subject: [PATCH 1/7] Implemented the option to anonymize or delete user applications in both wagtail & submission views --- hypha/apply/activity/models.py | 4 +- hypha/apply/activity/options.py | 1 + hypha/apply/funds/forms.py | 23 ++++ hypha/apply/funds/models/__init__.py | 3 +- hypha/apply/funds/models/submissions.py | 123 +++++++++++++++++- .../applicationsubmission_confirm_delete.html | 25 +++- .../funds/applicationsubmission_detail.html | 2 +- .../funds/includes/admin_primary_actions.html | 1 + .../modal_skeleton_submission_confirm.html | 33 +++++ .../templates/funds/submissions_result.html | 9 +- hypha/apply/funds/views/results.py | 7 +- hypha/apply/funds/views/submission_delete.py | 14 +- hypha/apply/funds/views/submission_edit.py | 4 +- hypha/apply/users/models.py | 46 +++++++ .../wagtailusers/users/confirm_delete.html | 38 ++++++ hypha/apply/users/templatetags/users_tags.py | 7 + hypha/apply/users/wagtail_hooks.py | 23 ++++ hypha/core/context_processors.py | 1 + hypha/settings/base.py | 2 + hypha/templates/cotton/modal/confirm.html | 8 +- 20 files changed, 348 insertions(+), 26 deletions(-) create mode 100644 hypha/apply/funds/templates/funds/includes/modal_skeleton_submission_confirm.html create mode 100644 hypha/apply/users/templates/wagtailusers/users/confirm_delete.html diff --git a/hypha/apply/activity/models.py b/hypha/apply/activity/models.py index c3d8cae455..13d1c81132 100644 --- a/hypha/apply/activity/models.py +++ b/hypha/apply/activity/models.py @@ -197,7 +197,7 @@ def get_absolute_url(self): class Activity(models.Model): timestamp = models.DateTimeField() type = models.CharField(choices=ACTIVITY_TYPES.items(), max_length=30) - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) source_content_type = models.ForeignKey( ContentType, @@ -373,7 +373,7 @@ class Event(models.Model): when = models.DateTimeField(auto_now_add=True) type = models.CharField(_("verb"), choices=MESSAGES.choices, max_length=50) by = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True ) content_type = models.ForeignKey( ContentType, blank=True, null=True, on_delete=models.CASCADE diff --git a/hypha/apply/activity/options.py b/hypha/apply/activity/options.py index 8798979e86..548bad5323 100644 --- a/hypha/apply/activity/options.py +++ b/hypha/apply/activity/options.py @@ -34,6 +34,7 @@ class MESSAGES(TextChoices): OPENED_SEALED = "OPENED_SEALED", _("opened sealed submission") REVIEW_OPINION = "REVIEW_OPINION", _("reviewed opinion") DELETE_SUBMISSION = "DELETE_SUBMISSION", _("deleted submission") + ANONYMIZE_SUBMISSION = "ANONYMIZE_SUBMISSION", _("anonymized submission") DELETE_REVIEW = "DELETE_REVIEW", _("deleted review") DELETE_REVIEW_OPINION = "DELETE_REVIEW_OPINION", _("deleted review opinion") CREATED_PROJECT = "CREATED_PROJECT", _("created project") diff --git a/hypha/apply/funds/forms.py b/hypha/apply/funds/forms.py index cab7bb1c0a..06e12112f3 100644 --- a/hypha/apply/funds/forms.py +++ b/hypha/apply/funds/forms.py @@ -5,6 +5,7 @@ import nh3 from django import forms +from django.conf import settings from django.db.models import Q from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -495,6 +496,28 @@ class Meta: fields = ["invited_user_email", "submission"] +class DeleteSubmissionForm(forms.Form): + # Alpine.js code added as an attribute to update confirmation text (ie. when a user has to type `delete` to finalize deletion) + anon_or_delete = forms.ChoiceField( + choices=[ + ("ANONYMIZE", "Anonymize submission"), + ("DELETE", "Delete submission"), + ], + widget=forms.RadioSelect( + attrs={ + "class": "text-sm radio-sm", + "@click": "mode = $el.value.toLowerCase()", + } + ), + initial="ANONYMIZE", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not settings.SUBMISSION_SKELETONING_ENABLED: + del self.fields["anon_or_delete"] + + class EditCoApplicantForm(forms.ModelForm): role = forms.ChoiceField( choices=CoApplicantRole.choices, label="Role", required=False diff --git a/hypha/apply/funds/models/__init__.py b/hypha/apply/funds/models/__init__.py index 8a4bba9e17..1985175984 100644 --- a/hypha/apply/funds/models/__init__.py +++ b/hypha/apply/funds/models/__init__.py @@ -14,13 +14,14 @@ from .reminders import Reminder from .reviewer_role import ReviewerRole, ReviewerSettings from .screening import ScreeningStatus -from .submissions import ApplicationSubmission +from .submissions import ApplicationSubmission, ApplicationSubmissionSkeleton __all__ = [ "ApplicationForm", "ApplicationRevision", "ApplicationSettings", "ApplicationSubmission", + "ApplicationSubmissionSkeleton", "AssignedReviewers", "Reminder", "ReviewerRole", diff --git a/hypha/apply/funds/models/submissions.py b/hypha/apply/funds/models/submissions.py index 01112ec0f2..3096ff3ea1 100644 --- a/hypha/apply/funds/models/submissions.py +++ b/hypha/apply/funds/models/submissions.py @@ -1,12 +1,13 @@ import operator from functools import partialmethod, reduce -from typing import Optional, Self +from typing import Any, Dict, Optional, Self from django.apps import apps from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractBaseUser, AnonymousUser, Group from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import SearchVector, SearchVectorField from django.core.exceptions import PermissionDenied @@ -486,7 +487,7 @@ class ApplicationSubmission( related_query_name="submission", ) user = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True ) search_data = models.TextField() search_document = SearchVectorField(null=True) @@ -1081,3 +1082,121 @@ def log_status_update(self, descriptor, source, target, **kwargs): user=by, source=instance, ) + + +class ApplicationSubmissionSkeleton(models.Model): + """The class to be used for stripping PII from an application and making it minimal""" + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True + ) + + value = models.FloatField(null=True) + + status = models.CharField( + max_length=100, + choices=get_all_possible_states(), + default=INITIAL_STATE, + ) + + category = ArrayField(models.CharField(), null=True) + + page = models.ForeignKey("wagtailcore.Page", on_delete=models.PROTECT) + round = models.ForeignKey( + "wagtailcore.Page", + on_delete=models.PROTECT, + related_name="skeleton_submissions", + null=True, + ) + + submit_time = models.DateTimeField( + verbose_name=_("submit time"), auto_now_add=False + ) + + screening_statuses = models.ManyToManyField( + "funds.ScreeningStatus", related_name="skeleton_submissions", blank=True + ) + + @classmethod + def from_dict( + cls, dict_submission: Dict[str, Any], save_user: bool = False + ) -> Self | None: + """Creates an ApplicationSubmissionSkeleton from a given dictionary + + Attempts to pull values from keys of `form_data`, `page_id`, `round_id`, `status`, `submit_time` and optionally (save_user=True) `user_id`. + + For convenience, it if values of previous keys are none will also try prepending `applicationsubmission__`. + ie. if `dict_submission.get("page_id") = None`, `dict_submission.get("applicationsubmission__page_id")` will be tried + + Args: + dict_submission: The dictionary containing the expected keys to create a ApplicationSubmissionSkeleton from + save_user: bool to save the provided user ID in `dict_submission` to the ApplicationSubmissionSkeleton + + Returns: Populated ApplicationSubmissionSkeleton if successful, None if not + """ + # If all values of the application dictionary are none, don't create a new skeleton app + if all(x is None for x in dict_submission.values()): + return None + + user = None + if save_user: + user = dict_submission.get("user_id") or dict_submission.get( + "applicationsubmission__user_id" + ) + + value = None + if form_data := dict_submission.get("form_data") or dict_submission.get( + "applicationsubmission__form_data" + ): + value = form_data.get("value") + + skeleton = ApplicationSubmissionSkeleton.objects.create( + user_id=user, + page_id=dict_submission.get("page_id") + or dict_submission.get("applicationsubmission__page_id"), + round_id=dict_submission.get("round_id") + or dict_submission.get("applicationsubmission__round_id"), + value=value, + status=dict_submission.get("status") + or dict_submission.get("applicationsubmission__status"), + submit_time=dict_submission.get("submit_time") + or dict_submission.get("applicationsubmission__submit_time"), + ) + + return skeleton + + @classmethod + def from_submission( + cls, submission: ApplicationSubmission, save_user: bool = False + ) -> Self: + """Creates an ApplicationSubmissionSkeleton from a given ApplicationSubmission object + + Note that this will NOT delete the provided ApplicationSubmission, just creates a ApplicationSubmissionSkeleton. + + Args: + submission: The ApplicationSubmission to create a ApplicationSubmissionSkeleton from + save_user: bool to save the user associated on the ApplicationSubmission to the ApplicationSubmissionSkeleton + + Returns: Populated ApplicationSubmissionSkeleton + """ + + user = None + if save_user: + user = submission.user + + skeleton = ApplicationSubmissionSkeleton.objects.create( + user=user, + page=submission.page, + round=submission.round, + value=submission.form_data.get("value", None), + status=submission.status, + submit_time=submission.submit_time, + ) + + # TODO: Handle categories here + + skeleton.screening_statuses.set(submission.screening_statuses.all()) + + skeleton.save() + + return skeleton diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_confirm_delete.html b/hypha/apply/funds/templates/funds/applicationsubmission_confirm_delete.html index cc40d9484f..22ca126583 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_confirm_delete.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_confirm_delete.html @@ -1,11 +1,22 @@ {% load i18n %} -

- {% blocktranslate trimmed %} - Are you sure you want to delete this application? All of your data for - this application will be permanently removed from our servers forever. - This action cannot be undone. - {% endblocktranslate %} -

+ {% if SUBMISSION_SKELETONING_ENABLED %} +

+ {% blocktranslate trimmed %} + If deleted, all of the data for this application will be permanently removed from our servers. Alternatively, the application can be anonymized resulting in identifying user information being removed while statistical information will be preserved. Neither action can be undone. + {% endblocktranslate %} +

+
+ {% for field in form.visible_fields %} + {{field}} + {% endfor %} +
+ {% else %} +

+ {% blocktranslate trimmed %} + If deleted, all of the data for this application will be permanently removed from our servers. This action cannot be undone. + {% endblocktranslate %} +

+ {% endif %}
diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html index 1149290e39..4073ed3a15 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html @@ -120,7 +120,7 @@
{% blocktrans with stage=object.previous.stage %}Your {{ stage }} applicatio href="{% url 'funds:submissions:delete' object.id %}" hx-get="{% url 'funds:submissions:delete' object.id %}" hx-target="#htmx-modal" - aria-label="{% trans "Delete Submission" %}" + aria-label="{% trans 'Delete Submission' %}" > {% heroicon_micro "trash" class="size-4" aria_hidden=true %} {% trans "Delete" %} diff --git a/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html b/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html index bc377fbf4e..c80a18e730 100644 --- a/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html +++ b/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html @@ -1,5 +1,6 @@ {% load i18n %} {% load heroicons primaryactions_tags translate_tags %} +{% load can from permission_tags %}

{% trans "Actions to take" %}

diff --git a/hypha/apply/funds/templates/funds/includes/modal_skeleton_submission_confirm.html b/hypha/apply/funds/templates/funds/includes/modal_skeleton_submission_confirm.html new file mode 100644 index 0000000000..5652c1b5bd --- /dev/null +++ b/hypha/apply/funds/templates/funds/includes/modal_skeleton_submission_confirm.html @@ -0,0 +1,33 @@ +{% load i18n static heroicons %} + +
+ {% csrf_token %} + +
+
+ {% heroicon_outline "exclamation-triangle" class="w-6 h-6 text-error" stroke_width="1.5" aria_hidden="true" %} +
+
+ +
+

+ {% blocktrans %} + Are you sure you want to anonymize this submission? Anonymizing submissions makes them inacccesible except via statistics in the "Results" view. + {% endblocktrans %} +

+
+
+
+ +
+ + +
+
diff --git a/hypha/apply/funds/templates/funds/submissions_result.html b/hypha/apply/funds/templates/funds/submissions_result.html index 012bf1001d..133e6fffd0 100644 --- a/hypha/apply/funds/templates/funds/submissions_result.html +++ b/hypha/apply/funds/templates/funds/submissions_result.html @@ -48,7 +48,6 @@

{% trans "Submissions" %} {{ submission_count }} -
@@ -66,6 +65,14 @@

{% trans "Submissions" %}

+
+
+ {% trans "Anonymized" %} +
+
+ {{ skeleton_count }} +
+
diff --git a/hypha/apply/funds/views/results.py b/hypha/apply/funds/views/results.py index 6f3d1a467f..10be092993 100644 --- a/hypha/apply/funds/views/results.py +++ b/hypha/apply/funds/views/results.py @@ -9,9 +9,7 @@ staff_required, ) -from ..models import ( - ApplicationSubmission, -) +from ..models import ApplicationSubmission, ApplicationSubmissionSkeleton from ..tables import ( SubmissionFilterAndSearch, ) @@ -50,12 +48,15 @@ def get_context_data(self, **kwargs): review_count = reviews.count() review_my_score = reviews.by_user(user).score() + skeleton_count = skeletons.count() + return super().get_context_data( submission_undetermined_count=submission_undetermined_count, review_my_count=review_my_count, submission_sum=submission_sum, submission_accepted_count=submission_accepted_count, submission_accepted_sum=submission_accepted_sum, + skeleton_count=skeleton_count, review_count=review_count, review_my_score=review_my_score, **kwargs, diff --git a/hypha/apply/funds/views/submission_delete.py b/hypha/apply/funds/views/submission_delete.py index 644befa402..5e673650de 100644 --- a/hypha/apply/funds/views/submission_delete.py +++ b/hypha/apply/funds/views/submission_delete.py @@ -5,8 +5,9 @@ from hypha.apply.activity.messaging import MESSAGES, messenger from hypha.apply.activity.models import Event +from hypha.apply.funds.forms import DeleteSubmissionForm -from ..models import ApplicationSubmission +from ..models import ApplicationSubmission, ApplicationSubmissionSkeleton from ..workflows.constants import DRAFT_STATE @@ -21,6 +22,7 @@ class SubmissionDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): """ model = ApplicationSubmission + form_class = DeleteSubmissionForm def test_func(self): return has_object_permission( @@ -33,12 +35,20 @@ def get_success_url(self): return reverse_lazy("funds:submissions:list") def form_valid(self, form): + print(form.cleaned_data) + submission = self.get_object() + message = MESSAGES.DELETE_SUBMISSION + + if form.cleaned_data.get("anon_or_delete") == "ANONYMIZE": + ApplicationSubmissionSkeleton.from_submission(submission) + message = MESSAGES.ANONYMIZE_SUBMISSION + # Notify unless author delete own draft. if submission.status != DRAFT_STATE and submission.user != self.request.user: messenger( - MESSAGES.DELETE_SUBMISSION, + message, user=self.request.user, request=self.request, source=submission, diff --git a/hypha/apply/funds/views/submission_edit.py b/hypha/apply/funds/views/submission_edit.py index 241b40c18e..a1a529fc4e 100644 --- a/hypha/apply/funds/views/submission_edit.py +++ b/hypha/apply/funds/views/submission_edit.py @@ -56,9 +56,7 @@ UpdateReviewersForm, UpdateSubmissionLeadForm, ) -from ..models import ( - ApplicationSubmission, -) +from ..models import ApplicationSubmission from ..permissions import ( has_permission, ) diff --git a/hypha/apply/users/models.py b/hypha/apply/users/models.py index 32d7471ccc..4c1fd71e48 100644 --- a/hypha/apply/users/models.py +++ b/hypha/apply/users/models.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.conf import settings from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser, BaseUserManager @@ -64,6 +65,30 @@ def finances(self): def contracting(self): return self.filter(groups__name=CONTRACTING_GROUP_NAME, is_active=True) + def delete(self, create_skeleton_submissions: bool = False): + submissions_to_skeleton = [] + if create_skeleton_submissions and settings.SUBMISSION_SKELETONING_ENABLED: + ApplicationSubmissionSkeleton = apps.get_model( + "funds", "ApplicationSubmissionSkeleton" + ) + submissions_to_skeleton = list( + self.values( + "applicationsubmission__form_data", + "applicationsubmission__page_id", + "applicationsubmission__round_id", + "applicationsubmission__status", + "applicationsubmission__submit_time", + ) + ) + + delete_return = super().delete() + + # Ensure account deletes successfully before skeletoning applications + for submission_dict in submissions_to_skeleton: + ApplicationSubmissionSkeleton.from_dict(submission_dict) + + return delete_return + class UserManager(BaseUserManager.from_queryset(UserQuerySet)): use_in_migrations = True @@ -326,6 +351,27 @@ def get_absolute_url(self): """ return reverse("wagtailusers_users:edit", args=[self.id]) + def delete( + self, create_skeleton_submissions: bool = False, using=None, keep_parents=False + ): + submissions_to_skeleton = [] + if create_skeleton_submissions and settings.SUBMISSION_SKELETONING_ENABLED: + ApplicationSubmissionSkeleton = apps.get_model( + "funds", "ApplicationSubmissionSkeleton" + ) + submissions_to_skeleton = list( + self.applicationsubmission_set.values( + "form_data", "page_id", "round_id", "status", "submit_time" + ) + ) + + delete_return = super().delete(using, keep_parents) + + for submission_dict in submissions_to_skeleton: + ApplicationSubmissionSkeleton.from_dict(submission_dict) + + return delete_return + class Meta: ordering = ("full_name", "email") diff --git a/hypha/apply/users/templates/wagtailusers/users/confirm_delete.html b/hypha/apply/users/templates/wagtailusers/users/confirm_delete.html new file mode 100644 index 0000000000..f080589a07 --- /dev/null +++ b/hypha/apply/users/templates/wagtailusers/users/confirm_delete.html @@ -0,0 +1,38 @@ +{% extends "wagtailadmin/generic/confirm_delete.html" %} +{% load i18n static users_tags wagtailadmin_tags %} + +{% block main_content %} +

{% trans "Are you sure you want to delete this user?" %}

+ {% get_user_submission_count user as submission_count %} + {% if submission_count %} +
+ {% icon name="warning" classname="" title="warning icon" %} + + {% if SUBMISSION_SKELETONING_ENABLED %} + {% blocktrans %}This will result in the user's {{submission_count}} associated submissions being deleted or anonymized!{% endblocktrans %} + {% else %} + {% blocktrans %}This will result in the user's {{submission_count}} associated submissions being deleted!{% endblocktrans %} + {% endif %} + +
+ {% endif %} +
+ {% csrf_token %} +
+
+ + +
+
+ + +
+
+ +
+{% endblock %} + + +{% block extra_css %} + +{% endblock %} diff --git a/hypha/apply/users/templatetags/users_tags.py b/hypha/apply/users/templatetags/users_tags.py index ecf44478bc..b8b65f1da9 100644 --- a/hypha/apply/users/templatetags/users_tags.py +++ b/hypha/apply/users/templatetags/users_tags.py @@ -1,4 +1,5 @@ from django import template +from django.apps import apps from django.utils.safestring import SafeString from django_otp import devices_for_user @@ -50,3 +51,9 @@ def tokens_text(token_set): @register.simple_tag() def user_image(identifier: str, size=20): return SafeString(get_identicon(identifier, size=size)) + + +@register.simple_tag() +def get_user_submission_count(user): + ApplicationSubmission = apps.get_model("funds", "ApplicationSubmission") + return ApplicationSubmission.objects.filter(user=user).count() diff --git a/hypha/apply/users/wagtail_hooks.py b/hypha/apply/users/wagtail_hooks.py index db12f370ab..b689c7e469 100644 --- a/hypha/apply/users/wagtail_hooks.py +++ b/hypha/apply/users/wagtail_hooks.py @@ -1,3 +1,5 @@ +from django.apps import apps +from django.conf import settings from wagtail import hooks from wagtail.models import Site @@ -35,6 +37,27 @@ def notify_after_edit_user(request, user): ) +@hooks.register("before_delete_user") +def anonymize_delete_user_submissions(request, user): + if ( + settings.SUBMISSION_SKELETONING_ENABLED + and request.method == "POST" + and request.POST.get("handle_subs") == "anon" + ): + ApplicationSubmissionSkeleton = apps.get_model( + "funds", "ApplicationSubmissionSkeleton" + ) + + submissions_to_skeleton = list( + user.applicationsubmission_set.values( + "form_data", "page_id", "round_id", "status", "submit_time" + ) + ) + + for submission_dict in submissions_to_skeleton: + ApplicationSubmissionSkeleton.from_dict(submission_dict) + + # Handle setting of `is_staff` after updating a user hooks.register("after_create_user", update_is_staff) hooks.register("after_edit_user", update_is_staff) diff --git a/hypha/core/context_processors.py b/hypha/core/context_processors.py index 5300a707be..479d040655 100644 --- a/hypha/core/context_processors.py +++ b/hypha/core/context_processors.py @@ -23,4 +23,5 @@ def global_vars(request): "SUBMISSIONS_TABLE_EXCLUDED_FIELDS": settings.SUBMISSIONS_TABLE_EXCLUDED_FIELDS, "HIJACK_ENABLE": settings.HIJACK_ENABLE, "LANGUAGE_SWITCHER": settings.LANGUAGE_SWITCHER, + "SUBMISSION_SKELETONING_ENABLED": settings.SUBMISSION_SKELETONING_ENABLED, } diff --git a/hypha/settings/base.py b/hypha/settings/base.py index e4feb55237..2fda59a7af 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -90,6 +90,8 @@ # Enable Projects in Hypha. Contracts and invoicing that comes after a submission is approved. PROJECTS_ENABLED = env.bool("PROJECTS_ENABLED", False) +SUBMISSION_SKELETONING_ENABLED = True + # Auto create projects for approved applications. PROJECTS_AUTO_CREATE = env.bool("PROJECTS_AUTO_CREATE", False) diff --git a/hypha/templates/cotton/modal/confirm.html b/hypha/templates/cotton/modal/confirm.html index 682f865974..72043313f0 100644 --- a/hypha/templates/cotton/modal/confirm.html +++ b/hypha/templates/cotton/modal/confirm.html @@ -4,7 +4,7 @@ class="px-4 pt-5 pb-4 sm:p-6" action="{{ request.path }}" method="post" - x-data="{userConfirmationInput: ''}" + x-data="{userConfirmationInput: '', delConfirmText: `{% trans 'delete' %}`, anonConfirmText: `{% trans 'anonymize' %}`, mode: 'delete'}" > {% csrf_token %} @@ -21,7 +21,7 @@