From 7214f0735feb4b5d458c220c4949eccbfdcc5e34 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Wed, 29 Jul 2026 14:27:54 -0400 Subject: [PATCH 01/21] Initial implementation for a comments sidebar on reports, invoices, PAF & SOW --- .../apply/activity/adapters/activity_feed.py | 8 + hypha/apply/activity/adapters/base.py | 4 + hypha/apply/activity/forms.py | 17 ++ .../migrations/0096_alter_event_type.py | 84 +----- hypha/apply/activity/options.py | 4 + .../activity/partials/comment_form.html | 37 +++ .../activity/ui/activity-action-item.html | 19 +- .../activity/ui/activity-comment-item.html | 268 ++++++++++++------ hypha/apply/activity/urls.py | 2 + hypha/apply/activity/views.py | 92 ++++++ .../apply/funds/templates/funds/comments.html | 2 +- hypha/apply/funds/views/comments.py | 1 - .../migrations/0108_projectformpointer.py | 39 +++ .../0109_create_project_form_pointers.py | 21 ++ hypha/apply/projects/models/payment.py | 6 +- hypha/apply/projects/models/project.py | 26 ++ hypha/apply/projects/reports/models.py | 5 + .../templates/reports/report_detail.html | 120 ++++---- .../includes/fetch_object_activity.html | 21 ++ .../application_projects/invoice_detail.html | 34 ++- .../partials/invoice_status.html | 74 ----- .../partials/object_status.html | 52 ++++ .../project_approval_detail.html | 159 ++++++----- .../project_sow_detail.html | 84 +++--- hypha/apply/projects/urls.py | 39 +++ hypha/apply/projects/views/__init__.py | 6 + hypha/apply/projects/views/payment.py | 18 +- hypha/apply/projects/views/project.py | 34 +++ .../apply/projects/views/project_partials.py | 116 ++++++-- .../apply/templates/forms/includes/field.html | 2 +- hypha/apply/utils/templatetags/apply_tags.py | 81 +++++- .../static_src/sass/components/_sidebar.scss | 2 +- .../includes/_toast-placeholder.html | 2 +- 33 files changed, 1027 insertions(+), 452 deletions(-) create mode 100644 hypha/apply/activity/templates/activity/partials/comment_form.html create mode 100644 hypha/apply/projects/migrations/0108_projectformpointer.py create mode 100644 hypha/apply/projects/migrations/0109_create_project_form_pointers.py create mode 100644 hypha/apply/projects/templates/application_projects/includes/fetch_object_activity.html delete mode 100644 hypha/apply/projects/templates/application_projects/partials/invoice_status.html create mode 100644 hypha/apply/projects/templates/application_projects/partials/object_status.html diff --git a/hypha/apply/activity/adapters/activity_feed.py b/hypha/apply/activity/adapters/activity_feed.py index 0c0a92817a..44e1c2ce25 100644 --- a/hypha/apply/activity/adapters/activity_feed.py +++ b/hypha/apply/activity/adapters/activity_feed.py @@ -50,6 +50,10 @@ class ActivityAdapter(AdapterBase): MESSAGES.CREATED_PROJECT: _( 'Created project with initial status of "{status}"' ), + MESSAGES.CREATED_SOW: _('Created SOW for project "{related.project}"'), + MESSAGES.EDITED_SOW: _('Edited SOW for project "{related.project}"'), + MESSAGES.CREATED_PF: _('Created project form for project "{related.project}"'), + MESSAGES.EDITED_PF: _('Edited project form for project "{related.project}"'), MESSAGES.PROJECT_TRANSITION: "handle_project_transition", MESSAGES.UPDATE_PROJECT_TITLE: _( "updated the project title from {old_title} to {source.title}" @@ -94,6 +98,10 @@ def extra_kwargs(self, message_type, source, sources, **kwargs): MESSAGES.DELETE_REVIEW_OPINION, MESSAGES.BATCH_REVIEWERS_UPDATED, MESSAGES.APPROVE_PROJECT, + MESSAGES.CREATED_SOW, + MESSAGES.EDITED_SOW, + MESSAGES.CREATED_PF, + MESSAGES.EDITED_PF, MESSAGES.REQUEST_PROJECT_CHANGE, MESSAGES.SEND_FOR_APPROVAL, MESSAGES.APPROVE_PAF, diff --git a/hypha/apply/activity/adapters/base.py b/hypha/apply/activity/adapters/base.py index 1617905464..3957b43209 100644 --- a/hypha/apply/activity/adapters/base.py +++ b/hypha/apply/activity/adapters/base.py @@ -21,6 +21,10 @@ MESSAGES.DELETE_REVIEW_OPINION: "review_opinion", MESSAGES.EDIT_REVIEW: "review", MESSAGES.CREATED_PROJECT: "submission", + MESSAGES.CREATED_SOW: "sow", + MESSAGES.EDITED_SOW: "sow", + MESSAGES.EDITED_PF: "pfp", + MESSAGES.CREATED_PF: "pfp", MESSAGES.PROJECT_TRANSITION: "old_stage", MESSAGES.APPROVE_PAF: "paf_approvals", # expect a list MESSAGES.UPDATE_PROJECT_LEAD: "old_lead", diff --git a/hypha/apply/activity/forms.py b/hypha/apply/activity/forms.py index fa8483dbc2..3e7fec6771 100644 --- a/hypha/apply/activity/forms.py +++ b/hypha/apply/activity/forms.py @@ -1,5 +1,6 @@ from django import forms from django.db import transaction +from django.forms.widgets import Textarea from django.utils.translation import gettext_lazy as _ from django_file_form.forms import FileFormMixin @@ -28,6 +29,10 @@ class Meta: "message", "visibility", "assign_to", + "related_content_type", + "related_object_id", + "source_content_type", + "source_object_id", ) labels = { "visibility": _("Visible to"), @@ -41,6 +46,10 @@ class Meta: widgets = { "visibility": forms.RadioSelect(), "message": PagedownWidget(), + "related_content_type": forms.HiddenInput(), + "related_object_id": forms.HiddenInput(), + "source_content_type": forms.HiddenInput(), + "source_object_id": forms.HiddenInput(), } def __init__(self, *args, user=None, has_coapplicants=False, **kwargs): @@ -78,3 +87,11 @@ def save(self, commit=True): ActivityAttachment(activity=instance, file=file) for file in added_files ) return instance + + +class CommentFormMini(CommentForm): + def __init__(self, *args, user=None, **kwargs): + super().__init__(*args, user=user, **kwargs) + self.fields["message"].widget = Textarea( + attrs={"rows": 2, "placeholder": "Write a comment"} + ) diff --git a/hypha/apply/activity/migrations/0096_alter_event_type.py b/hypha/apply/activity/migrations/0096_alter_event_type.py index f222ebbaf6..13dbba624b 100644 --- a/hypha/apply/activity/migrations/0096_alter_event_type.py +++ b/hypha/apply/activity/migrations/0096_alter_event_type.py @@ -1,90 +1,18 @@ -# Generated by Django 5.2.17 on 2026-08-25 18:01 +# Generated by Django 5.2.17 on 2026-08-31 14:07 from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ("activity", "0095_alter_event_type"), + ('activity', '0095_alter_event_type'), ] 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"), - ("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"), - ("ANONYMIZE_SUBMISSION", "anonymized 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"), - ( - "UPDATE_PROJECT_CONTRACT_NUMBER", - "updated project contract number", - ), - ("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"), - ("DELETE_REPORT", "deleted 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_ANONYMIZE_SUBMISSION", "batch anonymized 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"), - ("UPDATE_AUTHOR", "updated author"), - ("COMMENT_ASSIGNED", "comment assigned"), - ], - max_length=50, - verbose_name="verb", - ), + 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'), ('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'), ('ANONYMIZE_SUBMISSION', 'anonymized submission'), ('DELETE_REVIEW', 'deleted review'), ('DELETE_REVIEW_OPINION', 'deleted review opinion'), ('CREATED_PROJECT', 'created project'), ('CREATED_SOW', 'created a project SOW'), ('EDITED_SOW', 'edited a project SOW'), ('CREATED_PF', 'created a project form'), ('EDITED_PF', 'edited a project form'), ('UPDATE_PROJECT_LEAD', 'updated project lead'), ('UPDATE_PROJECT_TITLE', 'updated project title'), ('UPDATE_PROJECT_CONTRACT_NUMBER', 'updated project contract number'), ('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'), ('DELETE_REPORT', 'deleted 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_ANONYMIZE_SUBMISSION', 'batch anonymized 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'), ('UPDATE_AUTHOR', 'updated author'), ('COMMENT_ASSIGNED', 'comment assigned')], max_length=50, verbose_name='verb'), ), ] diff --git a/hypha/apply/activity/options.py b/hypha/apply/activity/options.py index fab645c22e..0b95a6ee72 100644 --- a/hypha/apply/activity/options.py +++ b/hypha/apply/activity/options.py @@ -36,6 +36,10 @@ class MESSAGES(TextChoices): DELETE_REVIEW = "DELETE_REVIEW", _("deleted review") DELETE_REVIEW_OPINION = "DELETE_REVIEW_OPINION", _("deleted review opinion") CREATED_PROJECT = "CREATED_PROJECT", _("created project") + CREATED_SOW = "CREATED_SOW", _("created a project SOW") + EDITED_SOW = "EDITED_SOW", _("edited a project SOW") + CREATED_PF = "CREATED_PF", _("created a project form") + EDITED_PF = "EDITED_PF", _("edited a project form") UPDATE_PROJECT_LEAD = "UPDATE_PROJECT_LEAD", _("updated project lead") UPDATE_PROJECT_TITLE = "UPDATE_PROJECT_TITLE", _("updated project title") UPDATE_PROJECT_CONTRACT_NUMBER = ( diff --git a/hypha/apply/activity/templates/activity/partials/comment_form.html b/hypha/apply/activity/templates/activity/partials/comment_form.html new file mode 100644 index 0000000000..28894a172d --- /dev/null +++ b/hypha/apply/activity/templates/activity/partials/comment_form.html @@ -0,0 +1,37 @@ +{% comment %} +Renders the sidebar comments form. + +Params: + form – the CommentFormMini form +{% endcomment %} + +{% load i18n static heroicons %} + +
+

Comment

+ {% csrf_token %} + + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} +
+ {% include "forms/includes/field.html" with field=form.message label_classes="sr-only" %} +
+ +
+ Additional options{% heroicon_mini 'chevron-down' class="size-5 group-open:-rotate-180" %} + {# Some small modifications to django-file-form to make it fit better with the smaller modal #} +
+ {% include "forms/includes/field.html" with field=form.visibility %} + {% include "forms/includes/field.html" with field=form.assign_to %} + {% include "forms/includes/field.html" with field=form.attachments %} + {{ form.related_content_type }} +
+
+ +
\ No newline at end of file diff --git a/hypha/apply/activity/templates/activity/ui/activity-action-item.html b/hypha/apply/activity/templates/activity/ui/activity-action-item.html index 25643ec769..e72b179853 100644 --- a/hypha/apply/activity/templates/activity/ui/activity-action-item.html +++ b/hypha/apply/activity/templates/activity/ui/activity-action-item.html @@ -1,7 +1,16 @@ +{% comment %} +Renders a line item of activity including the message attached to the activity & an icon based on the message. + +Params: + activity – an Activity object to render into a timeline + no_timeline - bool of should the line item be styled as a piece of a larger timeline + mini - bool that will force including only essential elements of the action item +{% endcomment %} + {% load i18n activity_tags heroicons %} {% with activity|display_for:request.user as activity_text %} -
+
@@ -22,6 +31,12 @@ {% heroicon_micro "lock-closed" class="inline" aria_hidden=true size=14 %} {% elif 'lead' in activity_text.lower or 'author' in activity_text.lower %} {% heroicon_micro "users" class="inline" aria_hidden=true size=14 %} + {% elif 'approved by' in activity_text.lower %} + {% heroicon_micro "check-circle" class="inline" aria_hidden=true size=14 %} + {% elif 'changes requested' in activity_text.lower %} + {% heroicon_micro "exclamation-circle" class="inline" aria_hidden=true size=14 %} + {% elif 'created' in activity_text.lower %} + {% heroicon_micro "folder-plus" class="inline" aria_hidden=true size=14 %} {% else %} {% heroicon_micro "eye" class="inline" aria_hidden=true size=15 %} {% endif %} @@ -39,7 +54,7 @@ {{ activity.timestamp|date:'SHORT_DATETIME_FORMAT' }} - {% if not submission_title and activity|user_can_see_related:request.user %} + {% if not submission_title and activity|user_can_see_related:request.user and not mini %} {% with url=activity.related_object.get_absolute_url %} {% if url %} 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 e93b6e3f45..50f29aeebc 100644 --- a/hypha/apply/activity/templates/activity/ui/activity-comment-item.html +++ b/hypha/apply/activity/templates/activity/ui/activity-comment-item.html @@ -1,23 +1,31 @@ +{% comment %} +Renders a comment activity item. Intended to be used as a part of a timeline + +Params: + activity – a comment Activity object to render into a timeline + mini - bool that will force including only essential elements of the comment item +{% endcomment %} + {% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags heroicons users_tags %}
-
- {% with activity|display_activity_author:request.user as author_name %} -
-
- + {% else %} +
+ {% with activity|display_activity_author:request.user as author_name %} +
+
+
+ {{ author_name }} -
- {% if submission_title %} - {% trans "updated" %} {{ activity.source.title }} - {% endif %} + {% if not request.user.is_applicant %} + + {% for role in activity.user.get_role_names %} + + {{ role }} + + {% endfor %} + + {% endif %} -
- {% include 'activity/partial_comment_message.html' with activity=activity %} -
+ + {% trans "commented" %} + {{ activity.timestamp|date:"SHORT_DATETIME_FORMAT" }} + +
+ +
+ {% 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 %} + {% trans "Assigned to you" %} + {% else %} + {% blocktrans with activity.assigned_to.full_name as assigned_to %}Assigned to {{ assigned_to }}{% endblocktrans %} + {% endif %} + + {% endif %} - {% if not submission_title and activity|user_can_see_related:request.user %} - {% with url=activity.related_object.get_absolute_url %} - {% if url %} - + {% with activity.visibility|visibility_display:request.user as visibility_text %} + + {% heroicon_outline "eye" size=14 class="inline" aria_hidden=true %} + {{ visibility_text }} + + {% endwith %} {% endif %} - {% endwith %} - {% endif %} + + {% 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 %} +
+
+ +
+ {% if submission_title %} + {% trans "updated" %} {{ activity.source.title }} + {% endif %} + +
+ {% include 'activity/partial_comment_message.html' with activity=activity %} +
+
-
- {% endwith %} -
+ {% endwith %} +
+ {% endif %}
diff --git a/hypha/apply/activity/urls.py b/hypha/apply/activity/urls.py index bd26613df1..f3c45dcd47 100644 --- a/hypha/apply/activity/urls.py +++ b/hypha/apply/activity/urls.py @@ -6,6 +6,7 @@ delete_comment, edit_comment, partial_comments, + post_comment, ) app_name = "activity" @@ -17,6 +18,7 @@ path("comments//", partial_comments, name="partial-comments"), path("/edit-comment/", edit_comment, name="edit-comment"), path("/delete-comment/", delete_comment, name="delete-comment"), + path("post-comment/", post_comment, name="post-comment"), path( "activities/attachment//download/", AttachmentView.as_view(), diff --git a/hypha/apply/activity/views.py b/hypha/apply/activity/views.py index 9933a2af42..b3387f4cbf 100644 --- a/hypha/apply/activity/views.py +++ b/hypha/apply/activity/views.py @@ -1,13 +1,21 @@ +import json + from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator +from django.http import Http404, HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, render +from django.utils import timezone from django.utils.decorators import method_decorator +from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ from django.views.decorators.http import require_http_methods from django.views.generic import ListView from rolepermissions.checkers import has_object_permission +from hypha.apply.activity.forms import CommentFormMini +from hypha.apply.activity.messaging import MESSAGES, messenger from hypha.apply.funds.models.submissions import ApplicationSubmission from hypha.apply.users.decorators import is_apply_staff, staff_required from hypha.apply.utils.storage import PrivateMediaView @@ -110,6 +118,90 @@ def delete_comment(request, pk): ) +@login_required +@require_http_methods(["POST", "GET"]) +@user_passes_test(is_apply_staff) +def post_comment(request: HttpRequest): + if request.method == "POST": + form = CommentFormMini(user=request.user, data=request.POST or None) + if (source_content_type := form.data["source_content_type"]) and ( + source_object_id := form.data["source_object_id"] + ): + if ( + not ContentType.objects.filter(id=source_content_type).exists() + or not ContentType.objects.get_for_id(source_content_type) + .model_class() + .objects.filter(id=source_object_id) + .exists() + ): + raise Http404 + + source = ContentType.objects.get_for_id( + source_content_type + ).get_object_for_this_type(id=source_object_id) + form.instance.user = request.user + form.instance.source = source + form.instance.type = COMMENT + form.instance.timestamp = timezone.now() + if form.is_valid(): + obj = form.save() + messenger( + MESSAGES.COMMENT, + request=request, + user=request.user, + source=source, + related=obj, + ) + return HttpResponse( + status=204, + headers={ + "HX-Trigger": json.dumps( + { + "commentAdded": obj.pk, + "showMessage": mark_safe(_("Comment added!")), + } + ), + }, + ) + return render( + request, "activity/partials/comment_form.html", {"form": form} + ) + else: + form = CommentFormMini(user=request.user) + + params = request.GET.dict() + + # Ensure the provided source content type & object actually exist + source_content_type = params.get("source_content_type") + source_object_id = params.get("source_object_id") + if not (source_content_type and source_object_id) or ( + not ContentType.objects.filter(id=source_content_type).exists() + or not ContentType.objects.get_for_id(source_content_type) + .model_class() + .objects.filter(id=source_object_id) + .exists() + ): + raise Http404 + + form.fields["source_content_type"].initial = source_content_type + form.fields["source_object_id"].initial = source_object_id + + if (related_content_type := params.get("related_content_type")) and ( + related_object_id := params.get("related_object_id") + ): + if ( + ContentType.objects.filter(id=related_content_type).exists() + and ContentType.objects.get_for_id(related_content_type) + .model_class() + .objects.filter(id=related_object_id) + .exists() + ): + form.fields["related_content_type"].initial = related_content_type + form.fields["related_object_id"].initial = related_object_id + + return render(request, "activity/partials/comment_form.html", {"form": form}) + + class ActivityContextMixin: """Mixin to add related 'comments' of the current view's 'self.object'""" diff --git a/hypha/apply/funds/templates/funds/comments.html b/hypha/apply/funds/templates/funds/comments.html index c6e9f3e3c8..9fec3457e6 100644 --- a/hypha/apply/funds/templates/funds/comments.html +++ b/hypha/apply/funds/templates/funds/comments.html @@ -57,7 +57,7 @@

{% trans "Add communication" %}

{% endfor %}
-
+
{% include "forms/includes/field.html" with field=form.message label_classes="sr-only" %}