From 4f268e484e69a16b38fa27e5f4d7ac5366a25b88 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Fri, 7 Feb 2025 15:01:29 -0500 Subject: [PATCH] Initial implementation of user mentions --- hypha/apply/activity/forms.py | 50 ++++-- hypha/apply/activity/services.py | 35 ++--- .../activity/include/comment_form.html | 148 ++++++++++++++++-- .../include/notifications_dropdown.html | 2 +- .../activity/partial_comment_message.html | 4 +- .../activity/ui/activity-comment-item.html | 6 +- .../activity/templatetags/activity_tags.py | 41 +++++ hypha/apply/activity/utils.py | 25 +++ .../partials/applicant_submissions.html | 2 +- .../funds/applicationsubmission_detail.html | 2 +- .../includes/screening_status_block.html | 2 +- .../funds/templatetags/submission_tags.py | 31 ---- hypha/apply/funds/tests/test_tags.py | 4 +- .../templates/review/review_detail.html | 6 +- hypha/apply/todo/options.py | 2 +- .../sass/components/_comment-suggestions.scss | 46 ++++++ hypha/static_src/sass/main.scss | 1 + 17 files changed, 315 insertions(+), 92 deletions(-) create mode 100644 hypha/apply/activity/utils.py delete mode 100644 hypha/apply/funds/templatetags/submission_tags.py create mode 100644 hypha/static_src/sass/components/_comment-suggestions.scss diff --git a/hypha/apply/activity/forms.py b/hypha/apply/activity/forms.py index 6b5544a590..dcdf2a0239 100644 --- a/hypha/apply/activity/forms.py +++ b/hypha/apply/activity/forms.py @@ -1,12 +1,16 @@ +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 @@ -14,19 +18,19 @@ 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", @@ -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: diff --git a/hypha/apply/activity/services.py b/hypha/apply/activity/services.py index 30244596c7..4b26f96a11 100644 --- a/hypha/apply/activity/services.py +++ b/hypha/apply/activity/services.py @@ -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 @@ -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 diff --git a/hypha/apply/activity/templates/activity/include/comment_form.html b/hypha/apply/activity/templates/activity/include/comment_form.html index fed4793699..38b358992c 100644 --- a/hypha/apply/activity/templates/activity/include/comment_form.html +++ b/hypha/apply/activity/templates/activity/include/comment_form.html @@ -1,4 +1,4 @@ -{% load i18n static %} +{% load i18n static activity_tags %}
{% 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 %}
+ diff --git a/hypha/apply/activity/templates/activity/include/notifications_dropdown.html b/hypha/apply/activity/templates/activity/include/notifications_dropdown.html index 03003c18c7..4e5b34bb34 100644 --- a/hypha/apply/activity/templates/activity/include/notifications_dropdown.html +++ b/hypha/apply/activity/templates/activity/include/notifications_dropdown.html @@ -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 %}

diff --git a/hypha/apply/activity/templates/activity/partial_comment_message.html b/hypha/apply/activity/templates/activity/partial_comment_message.html index dca46bdf01..9daf7def3d 100644 --- a/hypha/apply/activity/templates/activity/partial_comment_message.html +++ b/hypha/apply/activity/templates/activity/partial_comment_message.html @@ -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 %}

- {{ activity|display_for:request.user|submission_links|markdown|nh3 }} + {{ activity|display_for:request.user|submission_links:request.user|markdown|nh3 }}
{% if activity.edited %} 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 b56bb3578f..c7c34cd55e 100644 --- a/hypha/apply/activity/templates/activity/ui/activity-comment-item.html +++ b/hypha/apply/activity/templates/activity/ui/activity-comment-item.html @@ -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 %}
{% 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 %} {% heroicon_outline "user-plus" size=14 class="inline" aria_hidden=true %} @@ -57,7 +57,7 @@ {% blocktrans with activity.assigned_to.full_name as assigned_to %}Assigned to {{ assigned_to }}{% endblocktrans %} {% endif %} - {% endif %} + {% endif %} {% endcomment %} {% with activity.visibility|visibility_display:request.user as visibility_text %} str: def lowerfirst(value): """Lowercase the first character of the value.""" return value and value[0].lower() + value[1:] + + +@register.simple_tag +def get_org_faculty_auto_suggest() -> List[Dict]: + staff = User.objects.filter(groups__name__in=ROLES_ORG_FACULTY).distinct() + staff_list = [ + {"email": user.email.lower(), "display": user.get_display_name()} + for user in staff + ] + return staff_list + + +@register.filter +def submission_links(value: str, user: User): + # regex to find #id in a string, which id can be alphanumeric, underscore, hyphen + submission_matches = re.findall(r"(?{submission.title} #{submission.public_id or submission.id}' + ) + + if links: + for sid, link in links.items(): + value = re.sub(rf"(? str: + return rf"(?:^|(?<=\s))@({'|'.join(re.escape(email) for email in emails)})(?=\s|$)" + + +def format_comment_mentions(value, user) -> str: + faculty = User.objects.filter(groups__name__in=ROLES_ORG_FACULTY).distinct() + email_regex = get_mentioned_email_regex(faculty.values_list("email", flat=True)) + if faculty_matches := re.findall(email_regex, value): + qs = faculty.filter(email__in=faculty_matches) + + for mention in qs: + extra_class = " bg-yellow-200" if mention == user else "" + user_display = f'@{mention.get_display_name()}' + value = re.sub( + get_mentioned_email_regex([mention.email]), user_display, value + ) + + return value diff --git a/hypha/apply/dashboard/templates/dashboard/partials/applicant_submissions.html b/hypha/apply/dashboard/templates/dashboard/partials/applicant_submissions.html index ab86840c41..40bed80b69 100644 --- a/hypha/apply/dashboard/templates/dashboard/partials/applicant_submissions.html +++ b/hypha/apply/dashboard/templates/dashboard/partials/applicant_submissions.html @@ -1,4 +1,4 @@ -{% load i18n dashboard_statusbar_tags statusbar_tags workflow_tags heroicons submission_tags %} +{% load i18n dashboard_statusbar_tags statusbar_tags workflow_tags heroicons %} {% load can from permission_tags %} {% for submission in page.object_list %} diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html index fb131d7730..9e8d8bcad2 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html @@ -1,5 +1,5 @@ {% extends "base-apply.html" %} -{% load i18n static workflow_tags wagtailcore_tags statusbar_tags archive_tags submission_tags translate_tags %} +{% load i18n static workflow_tags wagtailcore_tags statusbar_tags archive_tags translate_tags %} {% load heroicons %} {% load can from permission_tags %} diff --git a/hypha/apply/funds/templates/funds/includes/screening_status_block.html b/hypha/apply/funds/templates/funds/includes/screening_status_block.html index 84767ddc4e..46512ee368 100644 --- a/hypha/apply/funds/templates/funds/includes/screening_status_block.html +++ b/hypha/apply/funds/templates/funds/includes/screening_status_block.html @@ -1,4 +1,4 @@ -{% load i18n submission_tags heroicons %} +{% load i18n heroicons %}
- {{ object.get_comments_display|submission_links }} + {{ object.get_comments_display|submission_links:request.user }}
- {{ object.output_answers|submission_links }} + {{ object.output_answers|submission_links:request.user }}
diff --git a/hypha/apply/todo/options.py b/hypha/apply/todo/options.py index 7d3159dc36..24d534e8c1 100644 --- a/hypha/apply/todo/options.py +++ b/hypha/apply/todo/options.py @@ -51,7 +51,7 @@ # ADD Manual Task COMMENT_TASK: { "text": _( - '{related.user} assigned you a comment on [{related.source.title}]({link} "{related.source.title}"):\n{msg}' + '{related.user} mentioned you in a comment on [{related.source.title}]({link} "{related.source.title}"):\n{msg}' ), "icon": "comment", "url": "{link}", diff --git a/hypha/static_src/sass/components/_comment-suggestions.scss b/hypha/static_src/sass/components/_comment-suggestions.scss new file mode 100644 index 0000000000..f5550b52ed --- /dev/null +++ b/hypha/static_src/sass/components/_comment-suggestions.scss @@ -0,0 +1,46 @@ +.comment-mirror { + position: absolute; + top: 28px; + left: 0; + height: 100%; + width: 100%; + overflow: hidden; + color: transparent; + z-index: -1; +} + +// .comment-suggestions { +// display: none; +// position: absolute; +// width: fit-content; +// z-index: 1000; +// background-color: white; +// border-radius: 0.25rem; +// box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); +// border-width: 1px; +// cursor: pointer; +// border-top-width: 1px; +// border-bottom-width: 0px; + +// &__option { +// padding-top: 0.25rem; +// padding-bottom: 0.25rem; +// padding-inline: 1rem; + +// &:not(:last-child) { +// border-bottom: 1px solid rgb(229, 231, 235); +// } + +// &:hover { +// background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); +// } + +// &--display { +// font-weight: bold; +// } + +// &--email { +// font-size: 0.9rem; +// } +// } +// } \ No newline at end of file diff --git a/hypha/static_src/sass/main.scss b/hypha/static_src/sass/main.scss index 893fc8f29b..ed0ef90844 100644 --- a/hypha/static_src/sass/main.scss +++ b/hypha/static_src/sass/main.scss @@ -49,6 +49,7 @@ @use "components/activity-notifications"; @use "components/dropdown"; @use "components/dashboard-table"; +@use "components/comment-suggestions"; // Layout @use "layout/header";