Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebuild communication and activity feed + assign a comment as task to user #3932

Merged
merged 6 commits into from
Oct 17, 2024
Merged
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 .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"pytestmark",
"ratelimit",
"SIGNUP",
"svgwrite",
"WAGTAILADMIN",
"wagtailcore"
]
Expand Down
58 changes: 28 additions & 30 deletions hypha/apply/activity/adapters/activity_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from hypha.apply.activity.models import ALL, APPLICANT, TEAM
from hypha.apply.activity.options import MESSAGES
from hypha.apply.funds.workflow import PHASE_BG_COLORS
from hypha.apply.projects.utils import (
get_invoice_public_status,
get_invoice_status_display_value,
Expand All @@ -26,12 +27,12 @@ class ActivityAdapter(AdapterBase):
MESSAGES.NEW_SUBMISSION: _(
"Submitted {source.title_text_display} for {source.page.title}"
),
MESSAGES.EDIT_SUBMISSION: _("Edited"),
MESSAGES.APPLICANT_EDIT: _("Edited"),
MESSAGES.UPDATE_LEAD: _("Lead changed from {old_lead} to {source.lead}"),
MESSAGES.BATCH_UPDATE_LEAD: _("Batch Lead changed to {new_lead}"),
MESSAGES.EDIT_SUBMISSION: _("edited the submission"),
MESSAGES.APPLICANT_EDIT: _("edited the submission"),
MESSAGES.UPDATE_LEAD: _("updated Lead from {old_lead} to {source.lead}"),
MESSAGES.BATCH_UPDATE_LEAD: _("batch updated Lead to {new_lead}"),
MESSAGES.DETERMINATION_OUTCOME: _(
"Sent a determination. Outcome: {determination.clean_outcome}"
"sent a determination. Outcome: {determination.clean_outcome}"
),
MESSAGES.BATCH_DETERMINATION_OUTCOME: "batch_determination",
MESSAGES.INVITED_TO_PROPOSAL: _("Invited to submit a proposal"),
Expand All @@ -42,24 +43,22 @@ class ActivityAdapter(AdapterBase):
MESSAGES.OPENED_SEALED: _("Opened the submission while still sealed"),
MESSAGES.SCREENING: "handle_screening_statuses",
MESSAGES.REVIEW_OPINION: _(
"{user} {opinion.opinion_display}s with {opinion.review.author}s review of {source}"
"{opinion.opinion_display}s with {opinion.review.author}s review of {source}"
),
MESSAGES.DELETE_REVIEW_OPINION: _(
"{user} deleted the opinion for review: {review_opinion.review}"
"deleted the opinion for review: {review_opinion.review}"
),
MESSAGES.CREATED_PROJECT: _("Created project"),
MESSAGES.PROJECT_TRANSITION: "handle_project_transition",
MESSAGES.UPDATE_PROJECT_TITLE: _(
"{user} has updated the project title from {old_title} to {source.title}"
),
MESSAGES.UPDATE_PROJECT_LEAD: _(
"Lead changed from {old_lead} to {source.lead}"
"updated the project title from {old_title} to {source.title}"
),
MESSAGES.UPDATE_PROJECT_LEAD: _("update Lead from {old_lead} to {source.lead}"),
MESSAGES.SEND_FOR_APPROVAL: _("Requested approval"),
MESSAGES.APPROVE_PAF: "handle_paf_assignment",
MESSAGES.APPROVE_PROJECT: _("Approved"),
MESSAGES.REQUEST_PROJECT_CHANGE: _(
'Requested changes for acceptance: "{comment}"'
'requested changes for acceptance: "{comment}"'
),
MESSAGES.SUBMIT_CONTRACT_DOCUMENTS: _("Submitted Contract Documents"),
MESSAGES.UPLOAD_CONTRACT: _("Uploaded a {contract.state} contract"),
Expand All @@ -69,17 +68,13 @@ class ActivityAdapter(AdapterBase):
MESSAGES.SUBMIT_REPORT: _("Submitted a report"),
MESSAGES.SKIPPED_REPORT: "handle_skipped_report",
MESSAGES.REPORT_FREQUENCY_CHANGED: "handle_report_frequency",
MESSAGES.DISABLED_REPORTING: _("Reporting disabled"),
MESSAGES.DISABLED_REPORTING: _("disabled reporting"),
MESSAGES.BATCH_DELETE_SUBMISSION: "handle_batch_delete_submission",
MESSAGES.BATCH_ARCHIVE_SUBMISSION: "handle_batch_archive_submission",
MESSAGES.BATCH_UPDATE_INVOICE_STATUS: "handle_batch_update_invoice_status",
MESSAGES.ARCHIVE_SUBMISSION: _(
"{user} has archived the submission: {source.title_text_display}"
),
MESSAGES.UNARCHIVE_SUBMISSION: _(
"{user} has unarchived the submission: {source.title_text_display}"
),
MESSAGES.DELETE_INVOICE: _("Deleted an invoice"),
MESSAGES.ARCHIVE_SUBMISSION: _("archived this submission"),
MESSAGES.UNARCHIVE_SUBMISSION: _("un-archived this submission"),
MESSAGES.DELETE_INVOICE: _("deleted an invoice"),
MESSAGES.REMOVE_TASK: "handle_task_removal",
}

Expand Down Expand Up @@ -214,28 +209,31 @@ def handle_paf_assignment(self, source, paf_approvals, **kwargs):
def handle_task_removal(self, source, task, **kwargs):
if task.user:
return _(
"{user} has removed the task {task.code} for {source} from the task list".format(
user=kwargs.get("user"), task=task, source=source
"removed the task {task.code} for {source} from the task list".format(
task=task, source=source
)
)
return _(
"{user} has removed the task {task.code} for {source} from whole team's{user_groups} task list.".format(
user=kwargs.get("user"),
"removed the task {task.code} for {source} from whole team's{user_groups} task list.".format(
task=task,
source=source,
user_groups=list(task.user_group.all().values_list("name", flat=True)),
)
)

def handle_transition(self, old_phase, source, **kwargs):
def wrap_in_color_class(text):
color_class = PHASE_BG_COLORS.get(text, "")
return f'<span class="rounded-full inline-block px-2 py-0.5 font-medium text-gray-800 {color_class}">{text}</span>'

submission = source
base_message = _("Progressed from {old_display} to {new_display}")

new_phase = submission.phase

staff_message = base_message.format(
old_display=old_phase.display_name,
new_display=new_phase.display_name,
old_display=wrap_in_color_class(old_phase.display_name),
new_display=wrap_in_color_class(new_phase.display_name),
)

if new_phase.permissions.can_view(submission.user):
Expand All @@ -246,8 +244,8 @@ def handle_transition(self, old_phase, source, **kwargs):
)

applicant_message = base_message.format(
old_display=old_phase.public_name,
new_display=new_phase.public_name,
old_display=wrap_in_color_class(old_phase.public_name),
new_display=wrap_in_color_class(new_phase.public_name),
)

return json.dumps(
Expand Down Expand Up @@ -363,10 +361,10 @@ def handle_screening_statuses(self, source, old_status, **kwargs):

if new_status and old_status != "-":
return _(
'Updated screening decision from "{old_status}" to "{new_status}".'
'Updated screening decision from "{old_status}" to "{new_status}"'
).format(old_status=old_status, new_status=new_status)
elif new_status:
return _('Added screening decision to "{new_status}".').format(
return _('Added screening decision to "{new_status}"').format(
new_status=new_status
)
elif old_status != "-":
Expand Down
26 changes: 24 additions & 2 deletions hypha/apply/activity/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@
from pagedown.widgets import PagedownWidget

from hypha.apply.stream_forms.fields import MultiFileField
from hypha.apply.todo.options import COMMENT_TASK
from hypha.apply.todo.views import add_task_to_user
from hypha.apply.users.models import STAFF_GROUP_NAME, User

from .models import Activity, ActivityAttachment


class CommentForm(FileFormMixin, forms.ModelForm):
attachments = MultiFileField(label=_("Attachments"), required=False)
assign_to = forms.ModelChoiceField(
queryset=User.objects.filter(groups__name=STAFF_GROUP_NAME),
required=False,
empty_label=_("Select..."),
label=_("Assign to"),
)

class Meta:
model = Activity
fields = ("message", "visibility")
fields = (
"message",
"visibility",
"assign_to",
)
labels = {
"visibility": "Visible to",
"message": "Message",
Expand Down Expand Up @@ -47,14 +60,23 @@ def __init__(self, *args, user=None, **kwargs):
visibility.choices = self.visibility_choices
visibility.initial = visibility.initial[0]
visibility.widget = forms.HiddenInput()
if not user.is_apply_staff:
self.fields["assign_to"].widget = forms.HiddenInput()

@transaction.atomic
def save(self, commit=True):
instance = super().save(commit=True)
added_files = self.cleaned_data["attachments"]
assigned_user = self.cleaned_data["assign_to"]
if assigned_user:
# add task to assigned user
add_task_to_user(
code=COMMENT_TASK,
user=assigned_user,
related_obj=instance,
)
if added_files:
ActivityAttachment.objects.bulk_create(
ActivityAttachment(activity=instance, file=file) for file in added_files
)

return instance
3 changes: 3 additions & 0 deletions hypha/apply/activity/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ class Meta:
ordering = ["-timestamp"]
base_manager_name = "objects"

def get_absolute_url(self):
return f"{self.source.get_absolute_url()}#communications--{self.id}"

@property
def priviledged(self):
# Not visible to applicant
Expand Down
69 changes: 67 additions & 2 deletions hypha/apply/activity/services.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import OuterRef, Subquery
from django.db.models.functions import JSONObject
from django.utils import timezone

from hypha.apply.todo.models import Task

from .models import Activity


def edit_comment(activity: Activity, message: str) -> Activity:
"""
Edit a comment by creating a clone of the original comment with the updated message.

Args:
activity (Activity): The original comment activity to be edited.
message (str): The new message to replace the original comment's message.

Returns:
Activity: The edited comment activity with the updated message.
"""
if message == activity.message:
return activity

# Create a clone of the comment to edit
previous = Activity.objects.get(pk=activity.pk)
previous.pk = None
previous.current = False
previous.save()

activity.previous = previous
activity.edited = timezone.now()
activity.message = message
activity.current = True
activity.save()

return activity


def get_related_actions_for_user(obj, user):
"""Return Activity objects related to an object, esp. useful with
ApplicationSubmission and Project.
Expand Down Expand Up @@ -37,11 +73,40 @@ def get_related_comments_for_user(obj, user):
"""
related_query = type(obj).activities.rel.related_query_name

return (
Activity.comments.filter(**{related_query: obj})
queryset = (
Activity.objects.filter(**{related_query: obj})
.exclude(current=False)
.select_related("user")
.prefetch_related(
"related_object",
)
.visible_to(user)
)

if user.is_apply_staff:
assigned_to_subquery = (
Task.objects.filter(
related_content_type=ContentType.objects.get_for_model(Activity),
related_object_id=OuterRef("id"),
)
.select_related("user")
.values(
json=JSONObject(
full_name="user__full_name", email="user__email", id="user__id"
)
)
)

queryset = queryset.annotate(assigned_to=Subquery(assigned_to_subquery))

return queryset


def get_comment_count(obj, user):
related_query = type(obj).activities.rel.related_query_name

return (
Activity.comments.filter(**{related_query: obj})
.exclude(current=False)
.visible_to(user)
).count()
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% load i18n %}

{% for action in actions %}
{% include "activity/include/listing_base.html" with activity=action %}
{% include "activity/ui/activity-action-item.html" with activity=action %}
{% empty %}
{% trans "There are no actions." %}
{% endfor %}

This file was deleted.

58 changes: 54 additions & 4 deletions hypha/apply/activity/templates/activity/include/comment_form.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,56 @@
{% load i18n %}
{% load i18n static %}

<div class="wrapper wrapper--comments">
{% trans "Submit" as submit %}
{% include "funds/includes/delegated_form_base.html" with form=comment_form value=submit extra_classes="form__comments" %}
<div class="wrapper wrapper--comments pb-4">
<form
class="form form__comments"
method="post"
id="{% if form_id %}{{ form_id }}{% else %}{{ comment_form.name }}{% endif %}"
enctype="multipart/form-data"
{% if action %}action="{{ action }}"{% endif %}
>
{% csrf_token %}

{{ comment_form.media }}
{% for hidden in comment_form.hidden_fields %}
{{ hidden }}
{% endfor %}

<div class="flex flex-wrap gap-4 lg:flex-nowrap lg:gap-8">
<div class="w-full lg:flex-1 -mt-4 max-w-[53rem]">
{% include "forms/includes/field.html" with field=comment_form.message label_classes="sr-only" %}

<div class="text-right">
<button
class="button button--primary w-full lg:w-auto"
id="{{ comment_form.name }}-submit"
name="{{ form_prefix }}{{ comment_form.name }}"
type="submit"
form="{% if form_id %}{{ form_id }}{% else %}{{ comment_form.name }}{% endif %}"
>
{% trans "Add Comment" %}
</button>
</div>
</div>

<div class="w-full lg:w-auto">
{% include "forms/includes/field.html" with field=comment_form.visibility %}
{% include "forms/includes/field.html" with field=comment_form.assign_to %}
{% include "forms/includes/field.html" with field=comment_form.attachments %}
</div>
</div>
</form>
<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('.id_assign_to select');

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