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
50 changes: 36 additions & 14 deletions hypha/apply/activity/forms.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import re

from django import forms
from django.db import transaction
from django.utils.translation import gettext_lazy as _
from django_file_form.forms import FileFormMixin

from hypha.apply.activity.utils import get_mentioned_email_regex
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 hypha.apply.users.models import User
from hypha.apply.users.roles import ROLES_ORG_FACULTY
from hypha.core.widgets import PagedownWidget

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"),
)
# 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",
"assign_to",
# "assign_to",
)
labels = {
"visibility": "Visible to",
Expand Down Expand Up @@ -60,19 +64,37 @@ 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()
# if not user.is_apply_staff:
# self.fields["assign_to"].widget = forms.HiddenInput()

def clean_message(self):
staff_emails = (
User.objects.filter(groups__name__in=ROLES_ORG_FACULTY)
.distinct()
.values_list("email", flat=True)
)
emails_regex = get_mentioned_email_regex(staff_emails)
cleaned_emails = re.findall(emails_regex, self.cleaned_data["message"])
users = User.objects.filter(email__in=cleaned_emails)
self.cleaned_data["mentions"] = users
return self.cleaned_data["message"]

@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
# 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,
# )
for user in self.cleaned_data.get("mentions", []):
add_task_to_user(
code=COMMENT_TASK,
user=assigned_user,
user=user,
related_obj=instance,
)
if added_files:
Expand Down
35 changes: 15 additions & 20 deletions hypha/apply/activity/services.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
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 @@ -83,21 +78,21 @@ 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))
# 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

Expand Down
148 changes: 137 additions & 11 deletions hypha/apply/activity/templates/activity/include/comment_form.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n static %}
{% load i18n static activity_tags %}

<div class="pb-4 wrapper wrapper--comments">
<form
Expand Down Expand Up @@ -33,23 +33,149 @@

<div class="w-full lg:max-w-[30%]">
{% 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.assign_to %} #}
{% include "forms/includes/field.html" with field=comment_form.attachments %}
</div>
</div>
</form>
<template id="suggestions-template">
{% get_org_faculty_auto_suggest as faculty %}
<div class="hidden absolute z-50 bg-white rounded border divide-y shadow-lg suggestions w-fit">
{% for user in faculty %}
<a class="hidden py-1 px-4 text-gray-800 cursor-pointer hover:bg-gray-100 focus:bg-gray-100" role="button">
<span class="font-bold display">{{ user.display }}</span>
<span class="text-sm email">{{ user.email }}</span>
</a>
{% endfor %}
</div>
</template>
<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' %}";
/*
Comment user tagging auto suggest
*/

const commentForm = document.getElementById("wmd-input-id_message");
const container = commentForm.parentNode;
const suggestionsTemplate = document.getElementById("suggestions-template").content.cloneNode(true);
const re = /(^|[^a-zA-z]+)@([a-zA-z-]*)/g

container.appendChild(suggestionsTemplate)
const suggestions = document.querySelector("div.suggestions");

const mirrored = document.createElement('div');
mirrored.textContent = commentForm.value;
mirrored.classList.add('comment-mirror');
container.prepend(mirrored);

const textareaStyles = window.getComputedStyle(commentForm);
[
'border',
'boxSizing',
'fontFamily',
'fontSize',
'fontWeight',
'letterSpacing',
'lineHeight',
'padding',
'textDecoration',
'textIndent',
'textTransform',
'whiteSpace',
'wordSpacing',
'wordWrap',
].forEach((property) => {
mirrored.style[property] = textareaStyles[property];
});
mirrored.style.borderColor = 'transparent';

const parseValue = (v) => v.endsWith('px') ? parseInt(v.slice(0, -2), 10) : 0;
const borderWidth = parseValue(textareaStyles.borderWidth);

const selectElements = document.querySelectorAll('.id_assign_to select');
const ro = new ResizeObserver(() => {
mirrored.style.width = `${commentForm.clientWidth + 2 * borderWidth}px`;
mirrored.style.height = `${commentForm.clientHeight + 2 * borderWidth}px`;
});
ro.observe(commentForm);

// add choices to all select elements
selectElements.forEach((selectElement) => {
new Choices(selectElement, {
removeItemButton: true,
allowHTML: true,
});
commentForm.addEventListener('scroll', () => {
mirrored.scrollTop = commentForm.scrollTop;
});

function indexOfGroup(match, n) {
var ix= match.index;
for (var i= 1; i<n; i++)
ix+= match[i].length;
return ix;
}

function insertString(start, end, substring, string){
return `${string.substring(0, start)}${substring}${string.substring(end)}`
}

function filterOptions(input, matchIndex, content) {
input = input.toLowerCase();
const staffOptions = Array.from(suggestions.querySelectorAll("a:has(span.email)"))
if (!staffOptions.length) {
return false
}
staffOptions.forEach((option) => {
option.style.display = 'none';
const email = option.querySelector("span.email").innerText.toLowerCase()
const display = option.querySelector("span.display").innerText.toLowerCase()
if (email.indexOf(input) > -1 || display.indexOf(input) > -1) {
option.style.display = 'block';
}
option.addEventListener('click', function() {
const newValue = insertString(matchIndex, matchIndex + input.length + 1, `@${email}`, content)
commentForm.value = newValue;
commentForm.focus();
commentForm.selectionStart = matchIndex + 1 + email.length
commentForm.selectionEnd = commentForm.selectionStart
suggestions.style.display = 'none';
});
})

return true
}

function suggestFaculty(event){
const content = event.target.value;
let m;
suggestions.style.display = "none"
do {
m = re.exec(content);
if (m) {
const matchIndex = indexOfGroup(m, 2)
const inputStr = m[2];
if (matchIndex + inputStr.length + 1 == event.target.selectionStart) {
if (!filterOptions(inputStr, matchIndex, content)) {
suggestions.style.display = 'none';
return;
}

const textBeforeCursor = content.substring(0, event.target.selectionStart);
const textAfterCursor = content.substring(event.target.selectionStart);

const pre = document.createTextNode(textBeforeCursor);
const post = document.createTextNode(textAfterCursor);
const caret = document.createElement('span');
caret.innerHTML = '&nbsp;';
caret.id = "caret";

mirrored.innerHTML = '';
mirrored.append(pre, caret, post);

const containerRect = container.getBoundingClientRect();
const caretRect = caret.getBoundingClientRect();
suggestions.style.top = `calc(${caretRect.top - containerRect.top}px + 2.5rem)`;
suggestions.style.left = `${caretRect.left - containerRect.left}px`;

suggestions.style.display = 'block';
}
}
} while (m);
}

commentForm.addEventListener('input', suggestFaculty, false);
</script>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags %}
{% load i18n activity_tags nh3_tags markdown_tags apply_tags %}

{% for activity in object_list %}
<p class="notifications__item">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% load heroicons activity_tags nh3_tags markdown_tags submission_tags apply_tags users_tags %}
{% load heroicons activity_tags nh3_tags markdown_tags apply_tags users_tags %}

<div class="max-w-none prose">
{{ activity|display_for:request.user|submission_links|markdown|nh3 }}
{{ activity|display_for:request.user|submission_links:request.user|markdown|nh3 }}
</div>

{% if activity.edited %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n activity_tags nh3_tags markdown_tags submission_tags apply_tags heroicons users_tags %}
{% load i18n activity_tags nh3_tags markdown_tags apply_tags heroicons users_tags %}

<article class="relative timeline-item">
<div
Expand Down Expand Up @@ -47,7 +47,7 @@
<div class="flex gap-1 items-center">
{% if not request.user.is_applicant %}

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

<span class="flex gap-1 items-center py-0.5 px-1.5 text-xs rounded-xl border border-gray-300 text-fg-muted">
{% heroicon_outline "user-plus" size=14 class="inline" aria_hidden=true %}
Expand All @@ -57,7 +57,7 @@
{% blocktrans with activity.assigned_to.full_name as assigned_to %}Assigned to {{ assigned_to }}{% endblocktrans %}
{% endif %}
</span>
{% endif %}
{% endif %} {% endcomment %}

{% with activity.visibility|visibility_display:request.user as visibility_text %}
<span class="flex gap-1 items-center py-0.5 px-1.5 text-xs uppercase rounded-xl border border-gray-300 text-fg-muted"
Expand Down
Loading