Skip to content
Closed
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
27 changes: 27 additions & 0 deletions docs/references/signals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Hypha uses django signals to enable extensions to modify behavior, as well as execute their own code when certain things happen in hypha. This page servers as a reference to implemented signals in hypha.

#### hypha.apply.activity.signals.message_hook

Gives a chance to override messages that go out from hypha, and whether those messages should go out.

Function definition to receive signal:

```
@receiver(message_hook, sender=sender)
def handle_message_hook(sender, message_type, **kwargs)
```

With the following parameters:

* `sender`: one of the adapters defined in `hypha/apply/activity/messaging.py`, such as EmailAdapter
* `message_type`: one of the `MESSAGES` defined in `hypha/apply/activity/options.py`
* `kwargs`: all the additional information present for generating the message. The information changes per call, so should be treated as unreliable. That said, normally there are a few items available
* `"request"` - the django request
* `"source"` - the item that caused the message to be fired, such as the submission, invoice, or project

The response from the hook should be a dictionary including any or all of the following attributes:

* `"should_send"` - A boolean about whether this message should be sent. If any hook handlers have this set to false, the message will not be sent.
* `"extra_kwargs"` - a dictionary of extra kwargs that are used by the adapters to send. For instance, "subject" is used by the email adapater for the subject line of the message
* `"message"` - The message that should be sent instead of the one generated by hypha
* `"priority"` - When multiple hooks are fired that define new messages or extra kwargs, the one with the highest priority will be used. If no priorities are defined, then the first one will be used.
Empty file.
11 changes: 11 additions & 0 deletions extensions/ots/message_configuration/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig


class MessageConfigurationConfig(AppConfig):
name = "extensions.ots.message_configuration"
label = "extension_message_configuration"

def ready(self):
from . import signals # noqa

pass
160 changes: 160 additions & 0 deletions extensions/ots/message_configuration/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Generated by Django 4.2.11 on 2024-04-30 19:58

from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields


class Migration(migrations.Migration):
initial = True

dependencies = [
("wagtailcore", "0089_log_entry_data_json_null_to_object"),
]

operations = [
migrations.CreateModel(
name="MessagingSetting",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("email_default_send", models.BooleanField(default=True)),
("slack_default_send", models.BooleanField(default=True)),
("email_footer", models.TextField(blank=True, null=True)),
("email_header", models.TextField(blank=True, null=True)),
(
"site",
models.OneToOneField(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to="wagtailcore.site",
),
),
],
options={
"verbose_name": "Messaging Settings",
},
),
migrations.CreateModel(
name="MessagingSettings",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"sort_order",
models.IntegerField(blank=True, editable=False, null=True),
),
(
"message_type",
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"),
("PARTNERS_UPDATED", "updated partners"),
("PARTNERS_UPDATED_PARTNER", "partners updated partner"),
("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"),
("DELETE_REVIEW", "deleted review"),
("DELETE_REVIEW_OPINION", "deleted review opinion"),
("CREATED_PROJECT", "created project"),
("UPDATED_VENDOR", "updated contracting information"),
("UPDATE_PROJECT_LEAD", "updated project lead"),
("EDIT_REVIEW", "edited review"),
("SEND_FOR_APPROVAL", "sent for approval"),
("APPROVE_PROJECT", "approved project"),
("ASSIGN_PAF_APPROVER", "assign paf approver"),
("APPROVE_PAF", "approved paf"),
("PROJECT_TRANSITION", "transitioned project"),
("REQUEST_PROJECT_CHANGE", "requested project change"),
(
"SUBMIT_CONTRACT_DOCUMENTS",
"submitted contract documents",
),
("UPLOAD_DOCUMENT", "uploaded document to project"),
("REMOVE_DOCUMENT", "removed document from 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"),
("SKIPPED_REPORT", "skipped report"),
("REPORT_FREQUENCY_CHANGED", "changed report frequency"),
("DISABLED_REPORTING", "disabled reporting"),
("REPORT_NOTIFY", "notified report"),
("CREATE_REMINDER", "created reminder"),
("DELETE_REMINDER", "deleted reminder"),
("REVIEW_REMINDER", "reminder to review"),
("BATCH_DELETE_SUBMISSION", "batch deleted submissions"),
("BATCH_ARCHIVE_SUBMISSION", "batch archive submissions"),
("STAFF_ACCOUNT_CREATED", "created new account"),
("STAFF_ACCOUNT_EDITED", "edited account"),
("ARCHIVE_SUBMISSION", "archived submission"),
("UNARCHIVE_SUBMISSION", "unarchived submission"),
],
max_length=50,
),
),
("email_subject", models.TextField(blank=True, null=True)),
("email_message", models.TextField(blank=True, null=True)),
("slack_message", models.TextField(blank=True, null=True)),
("email_enabled", models.BooleanField()),
("slack_enabled", models.BooleanField()),
(
"setting",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="messaging_settings",
to="extension_message_configuration.messagingsetting",
),
),
],
options={
"verbose_name": "Messaging Settings Page",
},
),
migrations.AddConstraint(
model_name="messagingsettings",
constraint=models.UniqueConstraint(
fields=("setting", "message_type"), name="unique_site_type"
),
),
]
Empty file.
100 changes: 100 additions & 0 deletions extensions/ots/message_configuration/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from django.db import models
from django.template.loader import get_template
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from wagtail.admin.panels import FieldPanel, HelpPanel, InlinePanel
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
from wagtail.models import Orderable

from hypha.apply.activity.options import MESSAGES


class MessagingHelpPanel(HelpPanel):
messages = {
MESSAGES.NEW_SUBMISSION: "messages/email_defaults/new_submission.html",
MESSAGES.DRAFT_SUBMISSION: "messages/email_defaults/draft_submission.html",
MESSAGES.DETERMINATION_OUTCOME: "messages/email_defaults/determination_outcome.html",
MESSAGES.INVITED_TO_PROPOSAL: "messages/email_defaults/invited_to_proposal.html",
MESSAGES.READY_FOR_REVIEW: "messages/email_defaults/ready_for_review.html",
MESSAGES.REVIEWERS_UPDATED: "messages/email_defaults/reviewers_updated.html",
MESSAGES.REVIEW_REMINDER: "messages/email_defaults/review_reminder.html",
MESSAGES.APPROVE_PAF: "messages/email_defaults/approve_paf.html",
MESSAGES.APPROVE_INVOICE: "messages/email_defaults/approve_invoice.html",
}

def __init__(self, *args, **kwargs):
self.email_messages = {}
for message in MESSAGES:
if message in MessagingHelpPanel.messages:
self.email_messages[message] = get_template(
MessagingHelpPanel.messages[message]
).render()
super().__init__("", "messaging_help.html", classname="messaging_help_panel")

class BoundPanel(HelpPanel.BoundPanel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.email_messages = self.panel.email_messages


@register_setting
class MessagingSetting(BaseSiteSetting, ClusterableModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! I'll tackle this depending on how the overall PR discussion goes :)

"""Allows settings per Message Type to be specified in Wagtail Settings and retrieved by the activity code.
This class affords a setting for a single Site to be referenced by the MessagingSettings model. This way
the BaseSiteSetting can be extended such that a setting appears in the Wagtail Settings menu but there can also be up to
one row per group for settings."""

email_default_send = models.BooleanField(default=True)
slack_default_send = models.BooleanField(default=True)
email_footer = models.TextField(null=True, blank=True)
email_header = models.TextField(null=True, blank=True)

class Meta:
verbose_name = "Messaging Settings"

panels = [
MessagingHelpPanel(),
FieldPanel("email_default_send"),
FieldPanel("slack_default_send"),
FieldPanel("email_header"),
FieldPanel("email_footer"),
InlinePanel(
"messaging_settings",
classname="all_messaging_settings",
),
]


class MessagingSettings(Orderable):
class Meta:
verbose_name = "Messaging Settings Page"
constraints = [
# There is a one-to-one relation for setting-to-site. Therefore, "setting" can be thought of as "site" here.
models.UniqueConstraint(
fields=["setting", "message_type"], name="unique_site_type"
),
]

setting = ParentalKey(
to=MessagingSetting,
on_delete=models.CASCADE,
related_name="messaging_settings",
)
message_type = models.CharField(
choices=MESSAGES.choices,
max_length=50,
)
email_subject = models.TextField(null=True, blank=True)
email_message = models.TextField(null=True, blank=True)
slack_message = models.TextField(null=True, blank=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two messages fields should be textareas I believe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see that!

email_enabled = models.BooleanField()
slack_enabled = models.BooleanField()

panels = [
FieldPanel("message_type", classname="message_type"),
FieldPanel("email_enabled"),
FieldPanel("email_subject"),
FieldPanel("email_message", classname="email_message"),
FieldPanel("slack_enabled"),
FieldPanel("slack_message", classname="slack_message"),
]
Loading