Skip to content

Commit 515dce8

Browse files
Add author updated feature to submission (#4380)
<!-- Thanks for contributing to Hypha! Please ensure your contributions pass all necessary linting/testing and that the appropriate documentation has been updated. --> <!-- Describe briefly what your pull request changes. If this is resolving an issue, please specify below via "Fixes #<Github Issue ID>" --> Fixes #4192 Staff can update the submission's author. Flow is similar to the Lead update but the button is a bit hidden in more actions. ## Test Steps <!-- If step does not require manual testing, skip/remove this section. Give a brief overview of the steps required for a user/dev to test this contribution. Important things to include: - Required user roles for where necessary (ie. "As a Staff Admin...") - Clear & validatable expected results (ie. "Confirm the submit button is now not clickable") - Language that can be understood by non-technical testers if being tested by users --> - [ ] Staff should find an option to 'Change Author' on the submission detail page in sidebar(More action). - [ ] Staff can select any applicant and assign them as an author of that submission. - [ ] New applicant should be notified about new assignment. - [ ] A slack notification should be there, like which user has replaced {old applicant} from {new_applicant}. - [ ] The new applicant's name should be updated in all the places on submission. - [ ] A new activity item should be created in the activity feed indicating the author update --------- Co-authored-by: Wes Appler <wes@opentech.fund>
1 parent c7618f4 commit 515dce8

17 files changed

Lines changed: 276 additions & 14 deletions

File tree

hypha/apply/activity/adapters/activity_feed.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class ActivityAdapter(AdapterBase):
7575
MESSAGES.UNARCHIVE_SUBMISSION: _("un-archived this submission"),
7676
MESSAGES.DELETE_INVOICE: _("deleted an invoice"),
7777
MESSAGES.REMOVE_TASK: "handle_task_removal",
78+
MESSAGES.UPDATE_AUTHOR: _("updated author from {old_author} to {source.user}"),
7879
}
7980

8081
def recipients(self, message_type, **kwargs):
@@ -100,6 +101,7 @@ def extra_kwargs(self, message_type, source, sources, **kwargs):
100101
MESSAGES.UNARCHIVE_SUBMISSION,
101102
MESSAGES.BATCH_ARCHIVE_SUBMISSION,
102103
MESSAGES.REMOVE_TASK,
104+
MESSAGES.UPDATE_AUTHOR,
103105
]:
104106
return {"visibility": TEAM}
105107

hypha/apply/activity/adapters/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
MESSAGES.REVIEW_REMINDER: "reminder",
4040
MESSAGES.BATCH_UPDATE_INVOICE_STATUS: "invoices",
4141
MESSAGES.REMOVE_TASK: "task",
42+
MESSAGES.UPDATE_AUTHOR: "old_author",
4243
}
4344

4445

hypha/apply/activity/adapters/emails.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class EmailAdapter(AdapterBase):
8181
MESSAGES.REPORT_NOTIFY: "messages/email/report_notify.html",
8282
MESSAGES.REVIEW_REMINDER: "messages/email/ready_to_review.html",
8383
MESSAGES.PROJECT_TRANSITION: "handle_project_transition",
84+
MESSAGES.UPDATE_AUTHOR: "messages/email/author_updated.html",
8485
}
8586

8687
def get_subject(self, message_type, source):

hypha/apply/activity/adapters/slack.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ class SlackAdapter(AdapterBase):
138138
MESSAGES.UNARCHIVE_SUBMISSION: _(
139139
"{user} has unarchived the submission: {source.title_text_display}"
140140
),
141+
MESSAGES.UPDATE_AUTHOR: _(
142+
"{user} has updated author from {old_author} to {source.user} for submission <{link}|{source}>"
143+
),
141144
}
142145

143146
def __init__(self):
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Generated by Django 5.2.14 on 2026-05-18 17:16
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("activity", "0092_alter_activity_user_alter_event_by_alter_event_type"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="event",
14+
name="type",
15+
field=models.CharField(
16+
choices=[
17+
("UPDATE_LEAD", "updated lead"),
18+
("BATCH_UPDATE_LEAD", "batch updated lead"),
19+
("EDIT_SUBMISSION", "edited submission"),
20+
("APPLICANT_EDIT", "edited applicant"),
21+
("NEW_SUBMISSION", "submitted new submission"),
22+
("DRAFT_SUBMISSION", "submitted new draft submission"),
23+
("SCREENING", "screened"),
24+
("TRANSITION", "transitioned"),
25+
("BATCH_TRANSITION", "batch transitioned"),
26+
("DETERMINATION_OUTCOME", "sent determination outcome"),
27+
("BATCH_DETERMINATION_OUTCOME", "sent batch determination outcome"),
28+
("INVITED_TO_PROPOSAL", "invited to proposal"),
29+
("REVIEWERS_UPDATED", "updated reviewers"),
30+
("BATCH_REVIEWERS_UPDATED", "batch updated reviewers"),
31+
("READY_FOR_REVIEW", "marked ready for review"),
32+
("BATCH_READY_FOR_REVIEW", "marked batch ready for review"),
33+
("NEW_REVIEW", "added new review"),
34+
("COMMENT", "added comment"),
35+
("PROPOSAL_SUBMITTED", "submitted proposal"),
36+
("OPENED_SEALED", "opened sealed submission"),
37+
("REVIEW_OPINION", "reviewed opinion"),
38+
("DELETE_SUBMISSION", "deleted submission"),
39+
("ANONYMIZE_SUBMISSION", "anonymized submission"),
40+
("DELETE_REVIEW", "deleted review"),
41+
("DELETE_REVIEW_OPINION", "deleted review opinion"),
42+
("CREATED_PROJECT", "created project"),
43+
("UPDATE_PROJECT_LEAD", "updated project lead"),
44+
("UPDATE_PROJECT_TITLE", "updated project title"),
45+
("EDIT_REVIEW", "edited review"),
46+
("SEND_FOR_APPROVAL", "sent for approval"),
47+
("APPROVE_PROJECT", "approved project"),
48+
("ASSIGN_PAF_APPROVER", "assign project form approver"),
49+
("APPROVE_PAF", "approved project form"),
50+
("PROJECT_TRANSITION", "transitioned project"),
51+
("REQUEST_PROJECT_CHANGE", "requested project change"),
52+
("SUBMIT_CONTRACT_DOCUMENTS", "submitted contract documents"),
53+
("UPLOAD_DOCUMENT", "uploaded document to project"),
54+
("UPLOAD_CONTRACT", "uploaded contract to project"),
55+
("APPROVE_CONTRACT", "approved contract"),
56+
("CREATE_INVOICE", "created invoice for project"),
57+
("UPDATE_INVOICE_STATUS", "updated invoice status"),
58+
("APPROVE_INVOICE", "approve invoice"),
59+
("DELETE_INVOICE", "deleted invoice"),
60+
("SENT_TO_COMPLIANCE", "sent project to compliance"),
61+
("UPDATE_INVOICE", "updated invoice"),
62+
("SUBMIT_REPORT", "submitted report"),
63+
("SKIPPED_REPORT", "skipped report"),
64+
("REPORT_FREQUENCY_CHANGED", "changed report frequency"),
65+
("DISABLED_REPORTING", "disabled reporting"),
66+
("REPORT_NOTIFY", "notified report"),
67+
("REVIEW_REMINDER", "reminder to review"),
68+
("BATCH_DELETE_SUBMISSION", "batch deleted submissions"),
69+
("BATCH_ANONYMIZE_SUBMISSION", "batch anonymized submissions"),
70+
("BATCH_ARCHIVE_SUBMISSION", "batch archive submissions"),
71+
("BATCH_INVOICE_STATUS_UPDATE", "batch update invoice status"),
72+
("STAFF_ACCOUNT_CREATED", "created new account"),
73+
("STAFF_ACCOUNT_EDITED", "edited account"),
74+
("ARCHIVE_SUBMISSION", "archived submission"),
75+
("UNARCHIVE_SUBMISSION", "unarchived submission"),
76+
("REMOVE_TASK", "remove task"),
77+
("INVITE_COAPPLICANT", "invite co-applicant"),
78+
("UPDATE_AUTHOR", "updated author"),
79+
],
80+
max_length=50,
81+
verbose_name="verb",
82+
),
83+
),
84+
]

hypha/apply/activity/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,4 @@ class MESSAGES(TextChoices):
8383
UNARCHIVE_SUBMISSION = "UNARCHIVE_SUBMISSION", _("unarchived submission")
8484
REMOVE_TASK = "REMOVE_TASK", _("remove task")
8585
INVITE_COAPPLICANT = "INVITE_COAPPLICANT", _("invite co-applicant")
86+
UPDATE_AUTHOR = "UPDATE_AUTHOR", _("updated author")

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
{% heroicon_micro "lock-open" class="inline" aria_hidden=true size=14 %}
2121
{% elif 'archived' in activity_text.lower %}
2222
{% heroicon_micro "lock-closed" class="inline" aria_hidden=true size=14 %}
23+
{% elif 'lead' in activity_text.lower or 'author' in activity_text.lower %}
24+
{% heroicon_micro "users" class="inline" aria_hidden=true size=14 %}
2325
{% else %}
2426
{% heroicon_micro "eye" class="inline" aria_hidden=true size=15 %}
2527
{% endif %}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{% extends "messages/email/applicant_base.html" %}
2+
3+
{% load i18n %}
4+
5+
{% block content %}{# fmt:off #}
6+
{% blocktrans with title=source.title %}You have been assigned as an Applicant to submission "{{ title }}".{% endblocktrans %}
7+
{% endblock %}
8+
9+
{% block more_info %}
10+
{% trans "Link to your submission" %}: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}
11+
{% trans "If you have any questions, please submit them here" %}: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}#communications
12+
13+
{% trans "See our guide for more information" %}: {{ ORG_GUIDE_URL }}
14+
15+
{% blocktrans %}If you have any issues accessing the submission or other general inquiries, please email us at {{ ORG_EMAIL }}{% endblocktrans %}
16+
{% endblock %}{# fmt:on #}

hypha/apply/funds/forms.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,31 @@ def make_role_reviewer_fields():
330330
return role_fields
331331

332332

333+
class UpdateAuthorForm(ApplicationSubmissionModelForm):
334+
author = forms.ModelChoiceField(
335+
queryset=User.objects.applicants(),
336+
label=_("Applicants"),
337+
required=True,
338+
)
339+
340+
class Meta:
341+
model = ApplicationSubmission
342+
fields: list = []
343+
344+
def __init__(self, *args, **kwargs):
345+
kwargs.pop("user")
346+
super().__init__(*args, **kwargs)
347+
self.fields["author"].queryset = User.objects.applicants().exclude(
348+
id=self.instance.user.id
349+
)
350+
self.fields["author"].widget.attrs.update({"data-js-choices": ""})
351+
352+
def save(self, *args, **kwargs):
353+
self.instance.user = self.cleaned_data["author"]
354+
self.instance.save()
355+
return self.instance
356+
357+
333358
class GroupedModelChoiceIterator(forms.models.ModelChoiceIterator):
334359
def __init__(self, field, groupby):
335360
self.groupby = groupby

hypha/apply/funds/permissions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,21 @@ def user_can_view_post_comment_form(user, submission):
288288
return True
289289

290290

291+
def can_change_submission_author(user, submission):
292+
if not user.is_authenticated:
293+
return False, "Login Required"
294+
295+
if user.is_apply_staff:
296+
return True, "Staff can update author"
297+
298+
return False, "Forbidden Error"
299+
300+
291301
permissions_map = {
292302
"submission_view": can_view_submission,
293303
"submission_edit": can_edit_submission,
294304
"submission_action": can_take_submission_actions,
305+
"change_author": can_change_submission_author,
295306
"can_view_submission_screening": can_view_submission_screening,
296307
"archive_alter": can_alter_archived_submissions,
297308
"co_applicant_invite": can_invite_co_applicants,

0 commit comments

Comments
 (0)