From 631796901eb13074e0c8fb5e095d1e347629a167 Mon Sep 17 00:00:00 2001 From: sandeepsajan0 Date: Thu, 6 Feb 2025 10:01:40 +0530 Subject: [PATCH 1/5] Add author updated feature to submission --- hypha/apply/activity/adapters/base.py | 1 + hypha/apply/activity/adapters/emails.py | 1 + hypha/apply/activity/adapters/slack.py | 3 + hypha/apply/activity/options.py | 1 + .../messages/email/author_updated.html | 16 +++++ hypha/apply/funds/forms.py | 28 +++++++++ hypha/apply/funds/permissions.py | 9 +++ .../funds/includes/admin_primary_actions.html | 2 + .../funds/modals/update_author_form.html | 11 ++++ hypha/apply/funds/urls.py | 6 ++ hypha/apply/funds/views/submission_edit.py | 58 +++++++++++++++++++ 11 files changed, 136 insertions(+) create mode 100644 hypha/apply/activity/templates/messages/email/author_updated.html create mode 100644 hypha/apply/funds/templates/funds/modals/update_author_form.html diff --git a/hypha/apply/activity/adapters/base.py b/hypha/apply/activity/adapters/base.py index 1ff0e03c45..3be7201ade 100644 --- a/hypha/apply/activity/adapters/base.py +++ b/hypha/apply/activity/adapters/base.py @@ -39,6 +39,7 @@ MESSAGES.REVIEW_REMINDER: "reminder", MESSAGES.BATCH_UPDATE_INVOICE_STATUS: "invoices", MESSAGES.REMOVE_TASK: "task", + MESSAGES.UPDATED_AUTHOR: "old_author", } diff --git a/hypha/apply/activity/adapters/emails.py b/hypha/apply/activity/adapters/emails.py index d8a95dfc63..42de0f36bc 100644 --- a/hypha/apply/activity/adapters/emails.py +++ b/hypha/apply/activity/adapters/emails.py @@ -81,6 +81,7 @@ class EmailAdapter(AdapterBase): MESSAGES.REPORT_NOTIFY: "messages/email/report_notify.html", MESSAGES.REVIEW_REMINDER: "messages/email/ready_to_review.html", MESSAGES.PROJECT_TRANSITION: "handle_project_transition", + MESSAGES.UPDATED_AUTHOR: "messages/email/author_updated.html", } def get_subject(self, message_type, source): diff --git a/hypha/apply/activity/adapters/slack.py b/hypha/apply/activity/adapters/slack.py index 4346aefaf7..cfdbbc94f1 100644 --- a/hypha/apply/activity/adapters/slack.py +++ b/hypha/apply/activity/adapters/slack.py @@ -134,6 +134,9 @@ class SlackAdapter(AdapterBase): MESSAGES.UNARCHIVE_SUBMISSION: _( "{user} has unarchived the submission: {source.title_text_display}" ), + MESSAGES.UPDATED_AUTHOR: _( + "{user} has updated author from {old_author} to {source.user} for submission <{link}|{source}>" + ), } def __init__(self): diff --git a/hypha/apply/activity/options.py b/hypha/apply/activity/options.py index 890cb88f3e..303487e39c 100644 --- a/hypha/apply/activity/options.py +++ b/hypha/apply/activity/options.py @@ -78,3 +78,4 @@ class MESSAGES(TextChoices): UNARCHIVE_SUBMISSION = "UNARCHIVE_SUBMISSION", _("unarchived submission") REMOVE_TASK = "REMOVE_TASK", _("remove task") INVITE_COAPPLICANT = "INVITE_COAPPLICANT", _("invite co-applicant") + UPDATED_AUTHOR = "UPDATED_AUTHOR", _("updated author") diff --git a/hypha/apply/activity/templates/messages/email/author_updated.html b/hypha/apply/activity/templates/messages/email/author_updated.html new file mode 100644 index 0000000000..cd091b5f83 --- /dev/null +++ b/hypha/apply/activity/templates/messages/email/author_updated.html @@ -0,0 +1,16 @@ +{% extends "messages/email/applicant_base.html" %} + +{% load i18n %} + +{% block content %}{# fmt:off #} +{% blocktrans with title=source.title %}You have assigned as an Applicant to submission "{{ title }}".{% endblocktrans %} +{% endblock %} + +{% block more_info %} +{% trans "Link to your submission" %}: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }} +{% trans "If you have any questions, please submit them here" %}: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}#communications + +{% trans "See our guide for more information" %}: {{ ORG_GUIDE_URL }} + +{% blocktrans %}If you have any issues accessing the submission or other general inquiries, please email us at {{ ORG_EMAIL }}{% endblocktrans %} +{% endblock %}{# fmt:on #} diff --git a/hypha/apply/funds/forms.py b/hypha/apply/funds/forms.py index 250190271f..d366d25c1c 100644 --- a/hypha/apply/funds/forms.py +++ b/hypha/apply/funds/forms.py @@ -330,6 +330,34 @@ def make_role_reviewer_fields(): return role_fields +class UpdateAuthorForm(ApplicationSubmissionModelForm): + author = forms.ModelChoiceField( + queryset=User.objects.applicants(), + label=_("Applicants"), + required=False, + ) + author.widget.attrs.update({"data-placeholder": "Select..."}) + + class Meta: + model = ApplicationSubmission + fields: list = [] + + def __init__(self, *args, **kwargs): + kwargs.pop("user") + super().__init__(*args, **kwargs) + current_author = self.instance.user + + author_field = self.fields["author"] + + # Removed current author from queryset + author_field.queryset = author_field.queryset.exclude(id=current_author.id) + author_field.initial = current_author + + def save(self, *args, **kwargs): + self.instance.user = self.cleaned_data["author"] + self.instance.save() + return self.instance + class GroupedModelChoiceIterator(forms.models.ModelChoiceIterator): def __init__(self, field, groupby): self.groupby = groupby diff --git a/hypha/apply/funds/permissions.py b/hypha/apply/funds/permissions.py index ef3c0f6f94..c0415ec3e7 100644 --- a/hypha/apply/funds/permissions.py +++ b/hypha/apply/funds/permissions.py @@ -286,12 +286,21 @@ def user_can_view_post_comment_form(user, submission): if co_applicant and co_applicant.role == CoApplicantRole.VIEW: return False return True +def can_change_submission_author(user, submission): + if not user.is_authenticated: + return False, "Login Required" + + if user.is_apply_staff: + return True, "Staff can update author" + + return False, "Forbidden Error" permissions_map = { "submission_view": can_view_submission, "submission_edit": can_edit_submission, "submission_action": can_take_submission_actions, + "change_author": can_change_submission_author, "can_view_submission_screening": can_view_submission_screening, "archive_alter": can_alter_archived_submissions, "co_applicant_invite": can_invite_co_applicants, 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 d3be0e427b..2c14b808a7 100644 --- a/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html +++ b/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html @@ -130,6 +130,8 @@

{% trans "Actions to take" %}

{% trans "Create Reminder" %} + + {% trans "Update Author" %} + +
+
+
{% trans "Current Author" %}
+
{{ object.user }} <{{object.user.email}}>
+
+ + {% include 'includes/dialog_form_base.html' with form=form value=value %} +
diff --git a/hypha/apply/funds/urls.py b/hypha/apply/funds/urls.py index 21b3c50b0b..40e074f52a 100644 --- a/hypha/apply/funds/urls.py +++ b/hypha/apply/funds/urls.py @@ -58,6 +58,7 @@ CreateProjectView, ProgressSubmissionView, SubmissionEditView, + UpdateAuthorView, UpdateLeadView, UpdateMetaTermsView, UpdateReviewersView, @@ -249,6 +250,11 @@ ReminderCreateView.as_view(), name="create_reminder", ), + path( + "author/change/", + UpdateAuthorView.as_view(), + name="change_author", + ), path( "translate/", TranslateSubmissionView.as_view(), diff --git a/hypha/apply/funds/views/submission_edit.py b/hypha/apply/funds/views/submission_edit.py index aa30a08872..a44a1a77da 100644 --- a/hypha/apply/funds/views/submission_edit.py +++ b/hypha/apply/funds/views/submission_edit.py @@ -51,6 +51,7 @@ from .. import services from ..forms import ( ProgressSubmissionForm, + UpdateAuthorForm, UpdateMetaTermsForm, UpdateReviewersForm, UpdateSubmissionLeadForm, @@ -641,6 +642,63 @@ def post(self, *args, **kwargs): @method_decorator(staff_required, name="dispatch") +class UpdateAuthorView(View): + model = ApplicationSubmission + form_class = UpdateAuthorForm + context_name = "author_form" + template = "funds/modals/update_author_form.html" + + def dispatch(self, request, *args, **kwargs): + self.submission = get_object_or_404(ApplicationSubmission, id=kwargs.get("pk")) + permission, reason = has_permission( + "change_author", + request.user, + object=self.submission, + raise_exception=False, + ) + if not permission: + messages.warning(self.request, reason) + return HttpResponseRedirect(self.submission.get_absolute_url()) + return super(UpdateAuthorView, self).dispatch(request, *args, **kwargs) + + def get(self, *args, **kwargs): + author_form = self.form_class(user=self.request.user, instance=self.submission) + return render( + self.request, + self.template, + context={ + "form": author_form, + "value": _("Update"), + "object": self.submission, + }, + ) + + def post(self, *args, **kwargs): + form = self.form_class( + self.request.POST, user=self.request.user, instance=self.submission + ) + old_author = self.submission.user + if form.is_valid(): + form.save() + + messenger( + MESSAGES.UPDATED_AUTHOR, + request=self.request, + user=self.request.user, + source=self.submission, + related=old_author, + ) + + messages.success(self.request, "Author updated successfully") + return HttpResponseClientRefresh() + + return render( + self.request, + self.template, + context={"form": form, "value": _("Update"), "object": self.submission}, + status=400, + ) + class UpdateMetaTermsView(View): template = "funds/modals/update_meta_terms_form.html" From 78224f84d356c7519359a9537f6425367b4f4607 Mon Sep 17 00:00:00 2001 From: sandeepsajan0 Date: Thu, 6 Feb 2025 10:08:51 +0530 Subject: [PATCH 2/5] Add migration for Updated author notification --- .../migrations/0092_alter_event_type.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 hypha/apply/activity/migrations/0092_alter_event_type.py diff --git a/hypha/apply/activity/migrations/0092_alter_event_type.py b/hypha/apply/activity/migrations/0092_alter_event_type.py new file mode 100644 index 0000000000..bd3cb3cd9e --- /dev/null +++ b/hypha/apply/activity/migrations/0092_alter_event_type.py @@ -0,0 +1,84 @@ +# Generated by Django 4.2.18 on 2025-02-06 04:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("activity", "0091_alter_activity_options_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="event", + name="type", + field=models.CharField( + choices=[ + ("UPDATE_LEAD", "updated lead"), + ("BATCH_UPDATE_LEAD", "batch updated lead"), + ("EDIT_SUBMISSION", "edited submission"), + ("APPLICANT_EDIT", "edited applicant"), + ("NEW_SUBMISSION", "submitted new submission"), + ("DRAFT_SUBMISSION", "submitted new draft submission"), + ("SCREENING", "screened"), + ("TRANSITION", "transitioned"), + ("BATCH_TRANSITION", "batch transitioned"), + ("DETERMINATION_OUTCOME", "sent determination outcome"), + ("BATCH_DETERMINATION_OUTCOME", "sent batch determination outcome"), + ("INVITED_TO_PROPOSAL", "invited to proposal"), + ("REVIEWERS_UPDATED", "updated reviewers"), + ("BATCH_REVIEWERS_UPDATED", "batch updated reviewers"), + ("PARTNERS_UPDATED", "updated partners"), + ("PARTNERS_UPDATED_PARTNER", "partners updated partner"), + ("READY_FOR_REVIEW", "marked ready for review"), + ("BATCH_READY_FOR_REVIEW", "marked batch ready for review"), + ("NEW_REVIEW", "added new review"), + ("COMMENT", "added comment"), + ("PROPOSAL_SUBMITTED", "submitted proposal"), + ("OPENED_SEALED", "opened sealed submission"), + ("REVIEW_OPINION", "reviewed opinion"), + ("DELETE_SUBMISSION", "deleted submission"), + ("DELETE_REVIEW", "deleted review"), + ("DELETE_REVIEW_OPINION", "deleted review opinion"), + ("CREATED_PROJECT", "created project"), + ("UPDATE_PROJECT_LEAD", "updated project lead"), + ("UPDATE_PROJECT_TITLE", "updated project title"), + ("EDIT_REVIEW", "edited review"), + ("SEND_FOR_APPROVAL", "sent for approval"), + ("APPROVE_PROJECT", "approved project"), + ("ASSIGN_PAF_APPROVER", "assign project form approver"), + ("APPROVE_PAF", "approved project form"), + ("PROJECT_TRANSITION", "transitioned project"), + ("REQUEST_PROJECT_CHANGE", "requested project change"), + ("SUBMIT_CONTRACT_DOCUMENTS", "submitted contract documents"), + ("UPLOAD_DOCUMENT", "uploaded document to project"), + ("UPLOAD_CONTRACT", "uploaded contract to project"), + ("APPROVE_CONTRACT", "approved contract"), + ("CREATE_INVOICE", "created invoice for project"), + ("UPDATE_INVOICE_STATUS", "updated invoice status"), + ("APPROVE_INVOICE", "approve invoice"), + ("DELETE_INVOICE", "deleted invoice"), + ("SENT_TO_COMPLIANCE", "sent project to compliance"), + ("UPDATE_INVOICE", "updated invoice"), + ("SUBMIT_REPORT", "submitted report"), + ("SKIPPED_REPORT", "skipped report"), + ("REPORT_FREQUENCY_CHANGED", "changed report frequency"), + ("DISABLED_REPORTING", "disabled reporting"), + ("REPORT_NOTIFY", "notified report"), + ("REVIEW_REMINDER", "reminder to review"), + ("BATCH_DELETE_SUBMISSION", "batch deleted submissions"), + ("BATCH_ARCHIVE_SUBMISSION", "batch archive submissions"), + ("BATCH_INVOICE_STATUS_UPDATE", "batch update invoice status"), + ("STAFF_ACCOUNT_CREATED", "created new account"), + ("STAFF_ACCOUNT_EDITED", "edited account"), + ("ARCHIVE_SUBMISSION", "archived submission"), + ("UNARCHIVE_SUBMISSION", "unarchived submission"), + ("REMOVE_TASK", "remove task"), + ("INVITE_COAPPLICANT", "invite co-applicant"), + ("UPDATED_AUTHOR", "updated author"), + ], + max_length=50, + verbose_name="verb", + ), + ), + ] From d2e8829d03ac7374533be431f42c32626a433ea7 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Tue, 12 May 2026 14:23:24 -0400 Subject: [PATCH 3/5] Updated PR to be more inline with current tech stack, added better handling of author display --- hypha/apply/funds/forms.py | 3 +- .../funds/applicationsubmission_detail.html | 4 +- .../apply/funds/templatetags/workflow_tags.py | 42 ++++++++++++++----- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/hypha/apply/funds/forms.py b/hypha/apply/funds/forms.py index d366d25c1c..58a7cf5cd2 100644 --- a/hypha/apply/funds/forms.py +++ b/hypha/apply/funds/forms.py @@ -336,7 +336,6 @@ class UpdateAuthorForm(ApplicationSubmissionModelForm): label=_("Applicants"), required=False, ) - author.widget.attrs.update({"data-placeholder": "Select..."}) class Meta: model = ApplicationSubmission @@ -352,12 +351,14 @@ def __init__(self, *args, **kwargs): # Removed current author from queryset author_field.queryset = author_field.queryset.exclude(id=current_author.id) author_field.initial = current_author + author_field.widget.attrs.update({"data-js-choices": ""}) def save(self, *args, **kwargs): self.instance.user = self.cleaned_data["author"] self.instance.save() return self.instance + class GroupedModelChoiceIterator(forms.models.ModelChoiceIterator): def __init__(self, field, groupby): self.groupby = groupby diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html index fe9d5b85d2..48aabc548d 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html @@ -93,7 +93,7 @@
{{ object.submit_time|date:"SHORT_DATETIME_FORMAT" }} - {% trans "by" %} {% display_submission_author %} + {% trans "by" %} {% get_author_display "submitted" %}
@@ -103,7 +103,7 @@
{% trans "by" %} - {% display_submission_author True %} + {% get_author_display "updated" %}
diff --git a/hypha/apply/funds/templatetags/workflow_tags.py b/hypha/apply/funds/templatetags/workflow_tags.py index 9ff2a4f99f..73343c1de0 100644 --- a/hypha/apply/funds/templatetags/workflow_tags.py +++ b/hypha/apply/funds/templatetags/workflow_tags.py @@ -1,3 +1,5 @@ +from typing import Literal + from django import template from django.conf import settings from django.utils.translation import gettext_lazy as _ @@ -51,30 +53,48 @@ def show_applicant_identity(submission: ApplicationSubmission, user: User) -> bo @register.simple_tag(takes_context=True) -def display_submission_author(context: dict, revision_author: bool = False) -> str: +def get_author_display(context: dict, type: Literal["submitted", "updated"]) -> str: """Creates a formatted author string based on the submission and viewer role. Args: - context: dict of template context - revision_author: if True, gets revision author. False (default) gets submission author + context: + dict of template context + type: + A string literal of either "submitted" (retrieves author of first revision), + or "updated" (retrieves author of live revision) Returns: - A string with the formatted author depending on the user role (ie. a - comment from staff viewed by an applicant will return the org name). + A string with the formatted author depending on the user role (ie. an edit from + staff viewed by an applicant will return the org name). """ submission: ApplicationSubmission = context["object"] request = context["request"] - author = submission.user if not revision_author else submission.live_revision.author - if ( - not revision_author or author == submission.user - ) and not show_applicant_identity(submission, request.user): + # Should the user's name be displayed + hide_pii = request.user != submission.user and not show_applicant_identity( + submission, request.user + ) + + author = None + # Edgecase handling of not having an existing revision happens for both submitted & updated + if type == "submitted": + first_revision = submission.revisions.order_by("timestamp").first() + if first_revision and (first_author := first_revision.author): + author = first_author + elif type == "updated": + live_revision = submission.live_revision + if live_revision and (live_author := live_revision.author): + author = live_author + + if author is None or hide_pii: return _("Applicant") - elif ( + + # If staff was to edit an application (likely an edge case) + if ( settings.HIDE_STAFF_IDENTITY and author.is_org_faculty and not request.user.is_org_faculty ): - return settings.ORG_LONG_NAME # Likely an edge case but covering bases + return settings.ORG_LONG_NAME return str(author) From 0d725ea826683f791a2aa71ed0df604c6ceb3ed5 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Tue, 12 May 2026 14:23:58 -0400 Subject: [PATCH 4/5] linting fixes --- hypha/apply/funds/permissions.py | 2 ++ hypha/apply/funds/views/submission_edit.py | 1 + 2 files changed, 3 insertions(+) diff --git a/hypha/apply/funds/permissions.py b/hypha/apply/funds/permissions.py index c0415ec3e7..6bb2b46289 100644 --- a/hypha/apply/funds/permissions.py +++ b/hypha/apply/funds/permissions.py @@ -286,6 +286,8 @@ def user_can_view_post_comment_form(user, submission): if co_applicant and co_applicant.role == CoApplicantRole.VIEW: return False return True + + def can_change_submission_author(user, submission): if not user.is_authenticated: return False, "Login Required" diff --git a/hypha/apply/funds/views/submission_edit.py b/hypha/apply/funds/views/submission_edit.py index a44a1a77da..d05448ba2c 100644 --- a/hypha/apply/funds/views/submission_edit.py +++ b/hypha/apply/funds/views/submission_edit.py @@ -699,6 +699,7 @@ def post(self, *args, **kwargs): status=400, ) + class UpdateMetaTermsView(View): template = "funds/modals/update_meta_terms_form.html" From 4779c15c452a3353238fa4b5c2c7d10a4dee7faa Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Tue, 12 May 2026 14:29:04 -0400 Subject: [PATCH 5/5] re-add migration --- hypha/apply/activity/migrations/0092_alter_event_type.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hypha/apply/activity/migrations/0092_alter_event_type.py b/hypha/apply/activity/migrations/0092_alter_event_type.py index bd3cb3cd9e..9718d7526c 100644 --- a/hypha/apply/activity/migrations/0092_alter_event_type.py +++ b/hypha/apply/activity/migrations/0092_alter_event_type.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.18 on 2025-02-06 04:38 +# Generated by Django 5.2.13 on 2026-05-12 18:28 from django.db import migrations, models @@ -28,8 +28,6 @@ class Migration(migrations.Migration): ("INVITED_TO_PROPOSAL", "invited to proposal"), ("REVIEWERS_UPDATED", "updated reviewers"), ("BATCH_REVIEWERS_UPDATED", "batch updated reviewers"), - ("PARTNERS_UPDATED", "updated partners"), - ("PARTNERS_UPDATED_PARTNER", "partners updated partner"), ("READY_FOR_REVIEW", "marked ready for review"), ("BATCH_READY_FOR_REVIEW", "marked batch ready for review"), ("NEW_REVIEW", "added new review"),