Skip to content

Commit

Permalink
Add comment assignment to user
Browse files Browse the repository at this point in the history
  • Loading branch information
theskumar committed Sep 21, 2024
1 parent ac884fa commit ecda2b4
Show file tree
Hide file tree
Showing 14 changed files with 227 additions and 113 deletions.
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
35 changes: 34 additions & 1 deletion hypha/apply/activity/services.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
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


Expand Down Expand Up @@ -68,7 +73,7 @@ def get_related_comments_for_user(obj, user):
"""
related_query = type(obj).activities.rel.related_query_name

return (
queryset = (
Activity.objects.filter(**{related_query: obj})
.exclude(current=False)
.select_related("user")
Expand All @@ -77,3 +82,31 @@ def get_related_comments_for_user(obj, user):
)
.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()
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 -mt-4">
{% 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">
<div class="w-full lg:flex-1 -mt-4">
{% 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.assign_to %}
{% include "forms/includes/field.html" with field=comment_form.visibility %}
{% 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>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags heroicons users_tags %}

<article class="timeline-item relative" id="communications#{{ activity.id }}">
<article class="timeline-item relative">
<div
class="flex py-3 -ms-8 md:-ms-20 before:block before:-z-10 before:absolute before:top-0 before:bottom-0 before:left-0 before:w-0.5 before:bg-slate-300"
>
Expand All @@ -13,45 +13,61 @@
</div>
</div>

<div class="{% if activity.user == request.user %}border-cyan-300{% else %}border-gray-300{% endif %} flex-1 pb-2 bg-white border rounded">
<div class="{% if activity.user == request.user %}border-cyan-300{% else %}border-gray-300{% endif %} flex-1 pb-2 bg-white border rounded target:ring-2 target:ring-cyan-400 ring-offset-2"
id="communications--{{ activity.id }}">
<header class="{% if activity.user == request.user %}bg-fuchsia-100{% else %}bg-slate-100{% endif %} flex flex-wrap items-center justify-between gap-2 px-3 py-2 rounded-t">
<div>
<span>
<strong class="font-semibold">{{ author_name }}</strong>

{% if activity.type == 'comment' %}
<span class="text-fg-muted">commented</span>
{% if not request.user.is_applicant %}
<span>
{% for role in activity.user.get_role_names %}
<span
class="text-fg-muted font-semibold inline-block px-2 py-0.5 text-sm border border-gray-300 rounded-xl"
data-tippy-content="This user is a {{ role }}"
>
{{ role }}
</span>
{% endfor %}
</span>
{% endif %}

<relative-time
class="text-fg-muted"
data-tippy-content="{{ activity.timestamp|date:"SHORT_DATETIME_FORMAT" }}"
datetime={{ activity.timestamp|date:"c" }}
>{{ activity.timestamp|date:"SHORT_DATETIME_FORMAT" }}</relative-time>
<a href="#communications--{{activity.id}}" class="hover:underline">
<span class="text-fg-muted">commented</span>
<relative-time
class="text-fg-muted"
data-tippy-content="{{ activity.timestamp|date:"SHORT_DATETIME_FORMAT" }}"
datetime={{ activity.timestamp|date:"c" }}
>{{ activity.timestamp|date:"SHORT_DATETIME_FORMAT" }}</relative-time>
</a>
</span>

{% if not request.user.is_applicant %}
<span>
{% for role in activity.user.get_role_names %}
<span
class="text-fg-muted font-semibold inline-block px-2 py-0.5 text-sm border border-gray-300 rounded-xl"
data-tippy-content="This user is a {{ role }}"
>
{{ role }}
</span>
{% endfor %}
</span>
{% endif %}

</div>
<div class="flex items-center gap-1">
{% if not request.user.is_applicant %}

{% if request.user.is_apply_staff and activity.assigned_to %}

<span class="flex items-center gap-1 text-fg-muted text-xs border rounded-xl border-gray-300 py-0.5 px-1.5">
{% 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 %}
</span>
{% endif %}

{% with activity.visibility|visibility_display:request.user as visibility_text %}
<span class="text-fg-muted text-xs flex items-center gap-1 uppercase border rounded-xl border-gray-300 py-0.5 px-1"
<span class="text-fg-muted text-xs flex items-center gap-1 uppercase border rounded-xl border-gray-300 py-0.5 px-1.5"
data-tippy-content="This is visible to {{ visibility_text }}">
{% heroicon_outline "eye" size=14 class="inline" aria_hidden=true %}
{{ visibility_text }}
</span>
{% endwith %}

{% endif %}

{% if editable and activity.user == request.user %}
Expand Down
5 changes: 4 additions & 1 deletion hypha/apply/activity/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ def get_context_data(self, **kwargs):
# are not homogeneous and this will fail
"comments": services.get_related_comments_for_user(
self.object, self.request.user
)
),
"comments_count": services.get_comment_count(
self.object, self.request.user
),
}
return super().get_context_data(**extra, **kwargs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ <h1 class="mt-2 mb-0 font-medium">{{ object.title }}<span class="text-gray-400">
x-init="$nextTick(() => { location.hash === '#communications' ? $dispatch('fetchComments') : '' })"
@hashchange.window="location.hash === '#communications' ? $dispatch('fetchComments') : ''"
>
{% trans "Conversations" %} ({{ comments.count }})
{% trans "Conversations" %} ({{ comments_count }})
</a>

<a class="tab__item"
Expand Down
41 changes: 33 additions & 8 deletions hypha/apply/templates/forms/includes/field.html
Original file line number Diff line number Diff line change
@@ -1,32 +1,54 @@
{% load i18n util_tags %}
{% load nh3_tags markdown_tags heroicons %}
{% with widget_type=field|widget_type field_type=field|field_type %}
{% load i18n util_tags nh3_tags markdown_tags heroicons %}

<div class="form__group {{ field.id_for_label }} form__group--{{ widget_type }} {% if widget_type == 'checkbox_input' %} form__group--checkbox{% endif %}{% if widget_type == 'clearable_file_input' or widget_type == 'multi_file_input' or widget_type == 'single_file_field_widget' or widget_type == 'multi_file_field_widget' %} form__group--file{% endif %}{% if field.help_text %} form__group--wrap{% endif %}{% if field.errors %} form__error{% endif %}{% if is_application and field.field.group_number > 1 %} field-group field-group-{{ field.field.group_number }}{% endif %}{% if is_application and field.field.grouper_for %} form-fields-grouper{% endif %}"{% if is_application and field.field.grouper_for %}data-grouper-for="{{ field.field.grouper_for }}" data-toggle-on="{{ field.field.choices.0.0 }}" data-toggle-off="{{ field.field.choices.1.0 }}"{% endif %}{% if is_application and field.field.group_number > 1 %} data-hidden="{% if not show_all_group_fields and not field.field.visible %}true{% else %}false{% endif %}" data-required="{{ field.field.required_when_visible }}"{% endif %}{% if field.field.word_limit %} data-word-limit="{{ field.field.word_limit }}"{% endif %}>
{% with widget_type=field|widget_type field_type=field|field_type %}
<div class="form__group {{ field.id_for_label }} form__group--{{ widget_type }} {% if widget_type == 'checkbox_input' %} form__group--checkbox{% endif %}{% if widget_type == 'clearable_file_input' or widget_type == 'multi_file_input' or widget_type == 'single_file_field_widget' or widget_type == 'multi_file_field_widget' %} form__group--file{% endif %}{% if field.help_text %} form__group--wrap{% endif %}{% if field.errors %} form__error{% endif %}{% if is_application and field.field.group_number > 1 %} field-group field-group-{{ field.field.group_number }}{% endif %}{% if is_application and field.field.grouper_for %} form-fields-grouper{% endif %}"
{% if is_application and field.field.grouper_for %}
data-grouper-for="{{ field.field.grouper_for }}"
data-toggle-on="{{ field.field.choices.0.0 }}"
data-toggle-off="{{ field.field.choices.1.0 }}"
{% endif %}
{% if is_application and field.field.group_number > 1 %}
data-hidden="{% if not show_all_group_fields and not field.field.visible %}true{% else %}false{% endif %}"
data-required="{{ field.field.required_when_visible }}"
{% endif %}
{% if field.field.word_limit %}
data-word-limit="{{ field.field.word_limit }}"
{% endif %}
>
{% if widget_type == 'clearable_file_input' or widget_type == 'multi_file_input' or widget_type == 'single_file_field_widget' or widget_type == 'multi_file_field_widget'%}
<span class="form__question form__file-label">
{{ field.label }}
{% if field.field.required %}
<span class="form__required">*</span>
{% endif %}
</span>
<label for="{{ field.id_for_label }}" class="mb-2 form__question form__question--{{ field_type }} {{ widget_type }}" {% if field.field.required %}required{% endif %}>
<label
for="{{ field.id_for_label }}"
class="mb-2 form__question form__question--{{ field_type }} {{ widget_type }}"
{% if field.field.required %}required{% endif %}
>
<span class="whitespace-nowrap">
{% heroicon_mini "arrow-up-tray" size=18 class="inline me-1" aria_hidden="true" %}
{% trans "Upload" %}
</span>
</label>
{% elif widget_type == 'checkbox_select_multiple' or widget_type == 'radio_select' %}
<fieldset>
<legend class="form__question form__question--{{ field_type }} {{ widget_type }}" {% if field.field.required %}required{% endif %}>
<legend class="form__question form__question--{{ field_type }} {{ widget_type }}"
{% if field.field.required %}required{% endif %}
>
<span>{{ field.label }}</span>
{% if field.field.required %}
<span class="form__required">*</span>
{% endif %}
</legend>
{% else %}
{% if not field.is_hidden and widget_type != 'checkbox_input' %}
<label for="{{ field.id_for_label }}" class="form__question form__question--{{ field_type }} {{ widget_type }}" {% if field.field.required %}required{% endif %}>
<label
for="{{ field.id_for_label }}"
class="form__question form__question--{{ field_type }} {{ widget_type }} {{ label_classes }}"
{% if field.field.required %}required{% endif %}
>
<span>{{ field.label }}</span>
{% if field.field.required %}
<span class="form__required">*</span>
Expand Down Expand Up @@ -65,7 +87,10 @@
<output class="form__file-list"></output>
{% endif %}
{% if widget_type == 'checkbox_input' %}
<label for="{{ field.id_for_label }}" class="form__question form__question--{{ field_type }} {{ widget_type }}" {% if field.field.required %}required{% endif %}>
<label for="{{ field.id_for_label }}"
class="form__question form__question--{{ field_type }} {{ widget_type }}"
{% if field.field.required %}required{% endif %}
>
<span>{{ field.label }}</span>
{% if field.field.required %}
<span class="form__required">*</span>
Expand Down
Loading

0 comments on commit ecda2b4

Please sign in to comment.