Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hypha/apply/activity/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
MESSAGES.REVIEW_REMINDER: "reminder",
MESSAGES.BATCH_UPDATE_INVOICE_STATUS: "invoices",
MESSAGES.REMOVE_TASK: "task",
MESSAGES.UPDATED_AUTHOR: "old_author",
}


Expand Down
1 change: 1 addition & 0 deletions hypha/apply/activity/adapters/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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):
Expand Down
3 changes: 3 additions & 0 deletions hypha/apply/activity/adapters/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,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):
Expand Down
83 changes: 83 additions & 0 deletions hypha/apply/activity/migrations/0087_alter_event_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Generated by Django 4.2.18 on 2025-02-06 04:38

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("activity", "0086_remove_django_messages_adapter"),
]

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"),
("UPDATED_AUTHOR", "updated author"),
],
max_length=50,
verbose_name="verb",
),
),
]
1 change: 1 addition & 0 deletions hypha/apply/activity/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ class MESSAGES(TextChoices):
ARCHIVE_SUBMISSION = "ARCHIVE_SUBMISSION", _("archived submission")
UNARCHIVE_SUBMISSION = "UNARCHIVE_SUBMISSION", _("unarchived submission")
REMOVE_TASK = "REMOVE_TASK", _("remove task")
UPDATED_AUTHOR = "UPDATED_AUTHOR", _("updated author")
16 changes: 16 additions & 0 deletions hypha/apply/activity/templates/messages/email/author_updated.html
Original file line number Diff line number Diff line change
@@ -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 #}
29 changes: 29 additions & 0 deletions hypha/apply/funds/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,35 @@ 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..."})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this author.widget.attrs.update({"data-placeholder": "Select...", "data-js-choices": ""})?


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 UpdatePartnersForm(ApplicationSubmissionModelForm):
partner_reviewers = forms.ModelMultipleChoiceField(
queryset=User.objects.partners(),
Expand Down
11 changes: 11 additions & 0 deletions hypha/apply/funds/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,20 @@ def can_view_submission_screening(user, submission):
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": is_user_has_access_to_view_submission,
"submission_edit": can_edit_submission,
"change_author": can_change_submission_author,
"can_view_submission_screening": can_view_submission_screening,
"archive_alter": can_alter_archived_submissions,
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ <h5>{% trans "Actions to take" %}</h5>
hx-target="#htmx-modal"
>{% trans "Create Reminder" %}</button>

<button
class="button button--white button--full-width button--bottom-space"
hx-get="{% url 'funds:submissions:change_author' pk=object.pk %}"
hx-target="#htmx-modal"
>{% trans "Change Author" %}</button>

<a class="button button--white button--full-width button--bottom-space"
href="{% url "apply:submissions:download" pk=object.pk %}"
>
Expand Down
37 changes: 37 additions & 0 deletions hypha/apply/funds/templates/funds/modals/update_author_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% load i18n static %}
{% modal_title %}{% trans "Update Author" %}{% endmodal_title %}

<form
class="p-4 form"
id="author_form"
hx-target="this"
hx-swap="outerHTML"
method="POST"
hx-post="{% url 'funds:submissions:change_author' pk=object.pk %}"
>
<div>
<dl class="mb-4">
<dt class="font-semibold">{% trans "Current Author" %}</dt>
<dd class="truncate">{{ object.user }} &lt;{{object.user.email}}&gt;</dd>
</dl>
</div>

{% url 'funds:submissions:change_author' pk=object.pk as author_update_url %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think this is used anywhere.

{% include 'funds/includes/dialog_form_base.html' with form=form value=value %}
</form>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "lead" modal is much simpler, no form in form. Here there seems to be two htmx calls, is that needed?

I see that the partner modal looks like above, I suspect that is overcomplicated as well.



<script type="module">
{% comment %} Do this here as the select elements for partners are dynamically generated. {% endcomment %}
import Choices from "{% static 'js/esm/choices.js-10-2-0.js' %}";

const selectElements = document.querySelectorAll('#author_form select');

// add choices to all select elements
selectElements.forEach((selectElement) => {
new Choices(selectElement, {
removeItemButton: true,
allowHTML: true,
});
});
</script>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With setting data-js-choices above I believe this script snippet is not needed.

6 changes: 6 additions & 0 deletions hypha/apply/funds/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
CreateProjectView,
ProgressSubmissionView,
SubmissionEditView,
UpdateAuthorView,
UpdateLeadView,
UpdateMetaTermsView,
UpdatePartnersView,
Expand Down Expand Up @@ -209,6 +210,11 @@
ReminderCreateView.as_view(),
name="create_reminder",
),
path(
"author/change/",
UpdateAuthorView.as_view(),
name="change_author",
),
path(
"translate/",
TranslateSubmissionView.as_view(),
Expand Down
60 changes: 60 additions & 0 deletions hypha/apply/funds/views/submission_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from .. import services
from ..forms import (
ProgressSubmissionForm,
UpdateAuthorForm,
UpdateMetaTermsForm,
UpdatePartnersForm,
UpdateReviewersForm,
Expand Down Expand Up @@ -641,6 +642,65 @@ 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,
)


@method_decorator(staff_required, name="dispatch")
class UpdatePartnersView(View):
model = ApplicationSubmission
Expand Down
Loading