Skip to content
Snippets Groups Projects
Commit 4c7bc983 authored by Tanner Prestegard's avatar Tanner Prestegard Committed by GraceDB
Browse files

Overhaul phone and email alerts

parent e4a8b099
No related branches found
No related tags found
No related merge requests found
...@@ -54,8 +54,18 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') ...@@ -54,8 +54,18 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
TWIML_BASE_URL = 'https://handler.twilio.com/twiml/' TWIML_BASE_URL = 'https://handler.twilio.com/twiml/'
# TwiML bin SIDs (for Twilio) # TwiML bin SIDs (for Twilio)
TWIML_BIN = { TWIML_BIN = {
'new': 'EH761b6a35102737e3d21830a484a98a08', 'event': {
'label_added': 'EHb596a53b9c92a41950ce1a47335fd834', 'new': 'EH761b6a35102737e3d21830a484a98a08',
'update': 'EH95d69491c166fbe8888a3b83b8aff4af',
'label_added': 'EHb596a53b9c92a41950ce1a47335fd834',
'label_removed': 'EH071c034f27f714bb7a832e85e6f82119',
},
'superevent': {
'new': 'EH5d4d61f5aee9f8687c5bc7d9d42acab9',
'update': 'EH35356707718e1b9a887c50359c3ab064',
'label_added': 'EH244c07ceeb152c6a374e4ffbd853e7a4',
'label_removed': 'EH9d796ce6a80e282a5c96e757e5c39406',
},
'test': 'EH6c0a168b0c6b011047afa1caeb49b241', 'test': 'EH6c0a168b0c6b011047afa1caeb49b241',
'verify': 'EHfaea274d4d87f6ff152ac39fea3a87d4', 'verify': 'EHfaea274d4d87f6ff152ac39fea3a87d4',
} }
......
from __future__ import absolute_import from __future__ import absolute_import
import logging import logging
import textwrap
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
...@@ -7,92 +8,145 @@ from django.urls import reverse ...@@ -7,92 +8,145 @@ from django.urls import reverse
from core.time_utils import gpsToUtc from core.time_utils import gpsToUtc
from core.urls import build_absolute_uri from core.urls import build_absolute_uri
from events.shortcuts import is_event
# Set up logger # Set up logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
EMAIL_SUBJECT_NEW = "[gracedb] {pipeline} event. ID: {graceid}"
EMAIL_MESSAGE_NEW = """ EMAIL_SUBJECT = {
New Event 'event': {
{group} / {pipeline} 'new': '[gracedb] {pipeline} event created: {graceid}',
GRACEID: {graceid} 'update': '[gracedb] {pipeline} event updated: {graceid}',
Info: {url} 'label_added': \
Data: {file_url} '[gracedb] {pipeline} event labeled with {label}: {graceid}',
Submitter: {submitter} 'label_removed': ('[gracedb] Label {label} removed from {pipeline} '
Event Summary: 'event: {graceid}'),
{summary} },
""" 'superevent': {
'new': '[gracedb] Superevent created: {sid}',
EMAIL_SUBJECT_LABEL = "[gracedb] {label} / {pipeline} / {search} / {graceid}" 'update': '[gracedb] Superevent updated: {sid}',
EMAIL_SUBJECT_LABEL_NOSEARCH = "[gracedb] {label} / {pipeline} / {graceid}" 'label_added': \
EMAIL_MESSAGE_LABEL = """ '[gracedb] Superevent labeled with {label}: {sid}',
A {pipeline} event with graceid {graceid} was labeled with {label}: {url} 'label_removed': \
""" '[gracedb] Label {label} removed from superevent: {sid}',
},
}
def indent(nindent, text):
return "\n".join([(nindent*' ')+line for line in text.split('\n')]) # es = 'Event' or 'Superevent'
# group_pipeline_ev_or_s = '{group} / {pipeline} event' or 'superevent'
EMAIL_BODY_TEMPLATE = textwrap.dedent("""\
def prepareSummary(event): {alert_type_verb} {group_pipeline_ev_or_s}: {graceid}
gpstime = event.gpstime
utctime = gpsToUtc(gpstime).strftime("%Y-%m-%d %H:%M:%S") {es} time (GPS): {gpstime}
instruments = getattr(event, 'instruments', "") {es} time (UTC): {utctime}
far = getattr(event, 'far', 1.0) {es} page: {detail_url}
summary_template = """ {es} data: {file_url}
Event Time (GPS): %s FAR: {far}
Event Time (UTC): %s Labels: {labels}
Instruments: %s """).rstrip()
FAR: %.3E """
summary = summary_template % (gpstime, utctime, instruments, far) ALERT_TYPE_VERBS = {
si_set = event.singleinspiral_set.all() 'new': 'New',
if si_set.count(): 'update': 'Updated',
si = si_set[0] 'label_added': 'Label {label} added to',
summary += """ 'label_removed': 'Label {label} removed from',
Component masses: %.2f, %.2f """ % (si.mass1, si.mass2) }
return summary
def prepare_email_body(event_or_superevent, alert_type, label=None):
def issue_email_alerts(event, recips, label=None):
# Sort out different cases for events or superevents
# Prepare URLs for email message body if is_event(event_or_superevent):
event_url = build_absolute_uri(reverse("view", args=[event.graceid])) group_pipeline_ev_or_s = '{group} / {pipeline} event'.format(
file_url = build_absolute_uri(reverse("file_list", args=[event.graceid])) group=event_or_superevent.group.name,
pipeline=event_or_superevent.pipeline.name)
# Compile subject and message content es = 'Event'
if label is None: detail_url = build_absolute_uri(reverse('view',
# Alert for new event args=[event_or_superevent.graceid]))
subject = EMAIL_SUBJECT_NEW.format(pipeline=event.pipeline.name, file_url = build_absolute_uri(reverse('file_list',
graceid=event.graceid) args=[event_or_superevent.graceid]))
message = EMAIL_MESSAGE_NEW.format(group=event.group.name, else:
pipeline=event.pipeline.name, graceid=event.graceid, group_pipeline_ev_or_s = 'superevent'
url=event_url, file_url=file_url, es = 'Superevent'
submitter=event.submitter.get_full_name(), detail_url = build_absolute_uri(reverse('superevents:view',
summary=indent(3, prepareSummary(event))) args=[event_or_superevent.graceid]))
file_url = build_absolute_uri(reverse('superevents:file-list',
args=[event_or_superevent.graceid]))
# Compile email body
alert_type_verb = ALERT_TYPE_VERBS[alert_type]
if label is not None and 'label' in alert_type:
alert_type_verb = alert_type_verb.format(label=label.name)
gpstime = event_or_superevent.gpstime
labels = None
if event_or_superevent.labels.exists():
labels = ", ".join(event_or_superevent.labels.values_list(
'name', flat=True))
email_body = EMAIL_BODY_TEMPLATE.format(
graceid=event_or_superevent.graceid,
alert_type_verb=alert_type_verb,
group_pipeline_ev_or_s=group_pipeline_ev_or_s,
es=es,
detail_url=detail_url,
file_url=file_url,
gpstime=gpstime,
utctime=gpsToUtc(gpstime).isoformat(),
far=event_or_superevent.far,
labels=labels)
# Add some extra information
has_si = False
if is_event(event_or_superevent):
instruments = getattr(event_or_superevent, 'instruments', None)
if instruments:
email_body += '\n Instruments: {inst}'.format(inst=instruments)
if event_or_superevent.singleinspiral_set.exists():
si = event_or_superevent.singleinspiral_set.first()
has_si = True
else:
if event_or_superevent.preferred_event.singleinspiral_set.exists():
si = event_or_superevent.preferred_event.singleinspiral_set.first()
has_si = True
if has_si:
email_body += '\n Component masses: {m1}, {m2}'.format(m1=si.mass1,
m2=si.mass2)
return email_body
def issue_email_alerts(event_or_superevent, alert_type, recipients,
label=None):
# Get subject template
if is_event(event_or_superevent):
event_type = 'event'
else:
event_type = 'superevent'
subject_template = EMAIL_SUBJECT[event_type][alert_type]
# Construct subject
subj_kwargs = {}
if label is not None and 'label' in alert_type:
subj_kwargs['label'] = label.name
if is_event(event_or_superevent):
subj_kwargs['graceid'] = event_or_superevent.graceid
subj_kwargs['pipeline'] = event_or_superevent.pipeline.name
else: else:
# Alert for label subj_kwargs['sid'] = event_or_superevent.superevent_id
if event.search: subject = subject_template.format(**subj_kwargs)
subject = EMAIL_SUBJECT_LABEL.format(label=label.name,
pipeline=event.pipeline.name, search=event.search.name, # Get email body
graceid=event.graceid) email_body = prepare_email_body(event_or_superevent, alert_type, label)
else:
subject = EMAIL_SUBJECT_LABEL_NOSEARCH.format(label=label.name,
pipeline=event.pipeline.name, graceid=event.graceid)
message = EMAIL_MESSAGE_LABEL.format(pipeline=event.pipeline.name,
graceid=event.graceid, label=label.name, url=event_url)
# Actual recipients should be BCC'd
bcc_addresses = [recip.email for recip in recips]
# Compile from/to addresses
from_address = settings.ALERT_EMAIL_FROM
to_addresses = settings.ALERT_EMAIL_TO
# Log email recipients # Log email recipients
logger.debug("Sending email to {recips}".format( logger.debug("Sending email to {recips}".format(
recips=", ".join(bcc_addresses))) recips=", ".join([r.email for r in recipients])))
# Send email # Send mail individually so all emails are not rejected due to a single
email = EmailMessage(subject, message, from_address, to_addresses, # address being blacklisted
bcc_addresses) for recip in recipients:
email.send() # Send email
email = EmailMessage(subject=subject, body=email_body,
from_email=settings.ALERT_EMAIL_FROM, to=[recip.email])
email.send()
from __future__ import absolute_import from __future__ import absolute_import
import json
import logging import logging
import os
import socket
from subprocess import Popen, PIPE, STDOUT
import sys
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group
from django.core.mail import EmailMessage
from django.db.models import QuerySet, Q
from django.urls import reverse
from core.time_utils import gpsToUtc
from events.models import Event
from events.permission_utils import is_external
from events.shortcuts import is_event from events.shortcuts import is_event
from search.query.labels import filter_for_labels
from superevents.shortcuts import is_superevent
from .models import Contact
from .email import issue_email_alerts from .email import issue_email_alerts
from .phone import issue_phone_alerts from .phone import issue_phone_alerts
from .recipients import ALERT_TYPE_RECIPIENT_GETTERS
from .xmpp import issue_xmpp_alerts from .xmpp import issue_xmpp_alerts
# Set up logger # Set up logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_alert_recips(event_or_superevent):
if is_superevent(event_or_superevent):
# TODO: update for superevents
pass
elif is_event(event_or_superevent):
event = event_or_superevent
# Queryset of all notifications for this pipeline
notifications = event.pipeline.notification_set.filter(labels=None)
# Filter on FAR threshold requirements
query = Q(far_threshold__isnull=True)
if event.far:
query |= Q(far_threshold__gt=event.far)
notifications = notifications.filter(query)
# Contacts for all notifications, make sure user is in LVC group (safeguard)
contacts = Contact.objects.filter(notification__in=notifications,
user__groups__name=settings.LVC_GROUP).select_related('user')
email_recips = contacts.exclude(email="")
phone_recips = contacts.exclude(phone="")
return email_recips, phone_recips
def get_alert_recips_for_label(event_or_superevent, label):
# Blank QuerySets for recipients
email_recips = Contact.objects.none()
phone_recips = Contact.objects.none()
# Construct a queryset containing only this object; needed for
# call to filter_for_labels
qs = event_or_superevent._meta.model.objects.filter(
pk=event_or_superevent.pk)
# Notifications on given label matching pipeline OR with no pipeline;
# no pipeline indicates that pipeline is irrelevant
if is_superevent(event_or_superevent):
# TODO: fix this
query = Q()
elif is_event(event_or_superevent):
event = event_or_superevent
query = Q(pipelines=event.pipeline) | Q(pipelines=None)
# Iterate over notifications found from the label query
# TODO: this doesn't work quite correctly since negated labels aren't
# properly handled in the view function which creates notifications.
# Example: a notification with '~INJ' as the label_query has INJ in its labels
# Idea: have filter_for_labels return a Q object generated from the
# label query
notifications = label.notification_set.filter(query).prefetch_related('contacts')
for notification in notifications:
if len(notification.label_query) > 0:
qs_out = filter_for_labels(qs, notification.label_query)
# If the label query cleans out our query set, we'll continue
# without adding the recipient.
if not qs_out.exists():
continue
# Compile a list of recipients from the notification's contacts.
# Require that the user is in the LVC group as a safeguard
contacts = notification.contacts.filter(user__groups__name=
settings.LVC_GROUP)
email_recips |= contacts.exclude(email="").select_related('user')
phone_recips |= contacts.exclude(phone="").select_related('user')
return email_recips, phone_recips
def issue_alerts(event_or_superevent, alert_type, serialized_object, def issue_alerts(event_or_superevent, alert_type, serialized_object,
serialized_parent=None, label=None): serialized_parent=None, **kwargs):
# Send XMPP alert # Send XMPP alert
if settings.SEND_XMPP_ALERTS: if settings.SEND_XMPP_ALERTS:
...@@ -105,37 +23,58 @@ def issue_alerts(event_or_superevent, alert_type, serialized_object, ...@@ -105,37 +23,58 @@ def issue_alerts(event_or_superevent, alert_type, serialized_object,
serialized_parent=serialized_parent) serialized_parent=serialized_parent)
# Below here, we only do processing for email and phone alerts ------------ # Below here, we only do processing for email and phone alerts ------------
if not (settings.SEND_EMAIL_ALERTS or settings.SEND_PHONE_ALERTS):
return
# TODO: make phone and e-mail alerts work for superevents # A few checks on whether we should issue a phone and/or email alert ------
if is_superevent(event_or_superevent): if not (settings.SEND_EMAIL_ALERTS or settings.SEND_PHONE_ALERTS):
return return
# Phone and email alerts are currently only for "new" and "label_added" # Phone and email alerts are only issued for certain alert types
# alert_types, for events only. # We check this by looking at the keys of ALERT_TYPE_RECIPIENT_GETTERS
if (alert_type not in ['new', 'label_added']): if (alert_type not in ALERT_TYPE_RECIPIENT_GETTERS):
return return
# Don't send phone or email alerts for MDC events or Test events # Don't send phone or email alerts for MDC or Test cases, or offline
if is_event(event_or_superevent): if is_event(event_or_superevent):
# Test/MDC events
event = event_or_superevent event = event_or_superevent
if ((event.search and event.search.name == 'MDC') \ if ((event.search and event.search.name == 'MDC') \
or event.group.name == 'Test'): or event.group.name == 'Test'):
return return
# Don't send them for offline events, either # Offline events
if event.offline: if event.offline:
return return
else:
# Test/MDC superevents
s = event_or_superevent
if (s.category in [s.__class__.SUPEREVENT_CATEGORY_TEST,
s.__class__.SUPEREVENT_CATEGORY_MDC]):
return
# Superevents with offline preferred events
if s.preferred_event.offline:
return
# Looks like we're going to issue phone and/or email alerts ---------------
# Get recipient getter class
rg_class = ALERT_TYPE_RECIPIENT_GETTERS[alert_type]
# Instantiate recipient getter
rg = rg_class(event_or_superevent, **kwargs)
# Get recipients
email_recipients, phone_recipients = rg.get_recipients()
if alert_type == "new": # Try to get label explicitly from kwargs
email_recips, phone_recips = get_alert_recips(event_or_superevent) label = kwargs.get('label', None)
elif alert_type == "label_added":
email_recips, phone_recips = \
get_alert_recips_for_label(event_or_superevent, label)
if settings.SEND_EMAIL_ALERTS and email_recips.exists(): # Issue email alerts
issue_email_alerts(event_or_superevent, email_recips, label=label) if settings.SEND_EMAIL_ALERTS and email_recipients.exists():
issue_email_alerts(event_or_superevent, alert_type, email_recipients,
label=label)
if settings.SEND_PHONE_ALERTS and phone_recips.exists(): # Issue phone alerts
issue_phone_alerts(event_or_superevent, phone_recips, label=label) if settings.SEND_PHONE_ALERTS and phone_recipients.exists():
issue_phone_alerts(event_or_superevent, alert_type, phone_recipients,
label=label)
from __future__ import absolute_import from __future__ import absolute_import
import logging import logging
import socket
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode
from django_twilio.client import twilio_client from django_twilio.client import twilio_client
from core.urls import build_absolute_uri from core.urls import build_absolute_uri
from events.permission_utils import is_external from events.permission_utils import is_external
from events.shortcuts import is_event
from .utils import convert_superevent_id_to_speech
# Set up logger # Set up logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# TODO: generalize to superevents
# Dict for managing TwiML bin arguments.
# Should match structure of TWIML_BINS dict in
# config/settings/secret.py.
TWIML_ARG_STR = {
'new': 'pipeline={pipeline}&graceid={graceid}&server={server}',
'label_added': ('pipeline={pipeline}&graceid={graceid}&label_lower={label}'
'&server={server}'),
}
# Dict for managing Twilio message contents. # Dict for managing Twilio message contents.
TWILIO_MSG_CONTENT = { TWILIO_MSG_CONTENT = {
'new': 'A {pipeline} event with GraceDB ID {graceid} was created. {url}', 'event': {
'label_added': ('A {pipeline} event with GraceDB ID {graceid} was labeled ' 'new': ('A {pipeline} event with GraceDB ID {graceid} was created. '
'with {label}. {url}'), '{url}'),
'update': ('A {pipeline} event with GraceDB ID {graceid} was updated. '
'{url}'),
'label_added': ('A {pipeline} event with GraceDB ID {graceid} was '
'labeled with {label}. {url}'),
'label_removed': ('The label {label} was removed from a {pipeline} '
'event with GraceDB ID {graceid}. {url}'),
},
'superevent': {
'new': 'A superevent with GraceDB ID {sid} was created. {url}',
'update': 'A superevent with GraceDB ID {sid} was updated. {url}',
'label_added': ('A superevent with GraceDB ID {sid} was labeled with '
'{label}. {url}'),
'label_removed': ('The label {label} was removed from a superevent '
'with GraceDB ID {sid}. {url}'),
},
} }
...@@ -39,40 +47,84 @@ def get_twilio_from(): ...@@ -39,40 +47,84 @@ def get_twilio_from():
raise RuntimeError('Could not determine "from" Twilio phone number') raise RuntimeError('Could not determine "from" Twilio phone number')
def issue_phone_alerts(event, contacts, label=None): def get_message_content(event_or_superevent, alert_type, **kwargs):
""" """Get content for text messages"""
USAGE: # kwargs should include 'label' for label_added and label_removed alerts
------
New event created:
issue_phone_alerts(event, contacts)
New label applied to event (Label is a GraceDB model):
issue_phone_alerts(event, contacts, label=Label)
Note: contacts is a QuerySet of Contact objects. # Get template
""" if is_event(event_or_superevent):
event_type = 'event'
# Determine alert_type else:
if label is not None: event_type = 'superevent'
alert_type = "label_added" msg_template = TWILIO_MSG_CONTENT[event_type][alert_type]
# Compile message content
if is_event(event_or_superevent):
event = event_or_superevent
# Get url
url = build_absolute_uri(reverse('view', args=[event.graceid]))
# Compile message
msg = msg_template.format(graceid=event.graceid,
pipeline=event.pipeline.name, url=url, **kwargs)
else: else:
alert_type = "new" superevent = event_or_superevent
# get url
url = build_absolute_uri(reverse('superevents:view',
args=[superevent.superevent_id]))
# Compile message
msg = msg_template.format(sid=superevent.superevent_id, url=url,
**kwargs)
return msg
def compile_twiml_url(event_or_superevent, alert_type, **kwargs):
# Try to get label from kwargs - should be a string corresponding
# to the label name here
label = kwargs.get('label', None)
# Compile urlparams
if is_event(event_or_superevent):
urlparams = {
'graceid': event_or_superevent.graceid,
'pipeline': event_or_superevent.pipeline.name,
}
twiml_bin_dict = settings.TWIML_BIN['event']
else:
urlparams = {'sid': convert_superevent_id_to_speech(
event_or_superevent.superevent_id)}
twiml_bin_dict = settings.TWIML_BIN['superevent']
if label is not None:
urlparams['label_lower'] = label.lower()
# Construct TwiML URL
twiml_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=twiml_bin_dict[alert_type],
params=urlencode(urlparams))
return twiml_url
def issue_phone_alerts(event_or_superevent, alert_type, contacts, label=None):
"""
Note: contacts is a QuerySet of Contact objects.
"""
# Get "from" phone number. # Get "from" phone number.
from_ = get_twilio_from() from_ = get_twilio_from()
# Compile Twilio voice URL and message body # Get message content
msg_params = { msg_kwargs = {}
'pipeline': event.pipeline.name, if alert_type in ['label_added', 'label_removed'] and label:
'graceid': event.graceid, msg_kwargs['label'] = label.name
'server': settings.SERVER_HOSTNAME, msg_body = get_message_content(event_or_superevent, alert_type,
'url': build_absolute_uri(reverse('view', args=[event.graceid])), **msg_kwargs)
}
if alert_type == "label_added": # Compile Twilio voice URL
msg_params['label'] = label.name twiml_url = compile_twiml_url(event_or_superevent, alert_type,
twiml_url = settings.TWIML_BASE_URL + settings.TWIML_BIN[alert_type] + \ **msg_kwargs)
"?" + TWIML_ARG_STR[alert_type]
twiml_url = twiml_url.format(**msg_params)
msg_body = TWILIO_MSG_CONTENT[alert_type].format(**msg_params)
# Loop over recipients and make calls and/or texts. # Loop over recipients and make calls and/or texts.
for contact in contacts: for contact in contacts:
...@@ -81,23 +133,29 @@ def issue_phone_alerts(event, contacts, label=None): ...@@ -81,23 +133,29 @@ def issue_phone_alerts(event, contacts, label=None):
# shouldn't even be able to sign up for phone alerts, # shouldn't even be able to sign up for phone alerts,
# but this is another safety measure. # but this is another safety measure.
logger.warning("External user {0} is somehow signed up for" logger.warning("External user {0} is somehow signed up for"
" phone alerts".format(contact.user.username)) " phone alerts".format(contact.user.username))
continue continue
try: try:
# POST to TwiML bin to make voice call. if (contact.phone_method in [contact.__class__.CONTACT_PHONE_CALL,
if contact.call_phone: contact.__class__.CONTACT_PHONE_BOTH]):
# POST to TwiML bin to make voice call.
logger.debug("Calling {0} at {1}".format(contact.user.username, logger.debug("Calling {0} at {1}".format(contact.user.username,
contact.phone)) contact.phone))
twilio_client.calls.create(to=contact.phone, from_=from_, twilio_client.calls.create(to=contact.phone, from_=from_,
url=twiml_url, method='GET') url=twiml_url, method='GET')
except Exception as e:
logger.exception("Failed to call {0} at {1}.".format(
contact.user.username, contact.phone))
try:
# Create Twilio message. # Create Twilio message.
if contact.text_phone: if (contact.phone_method in [contact.__class__.CONTACT_PHONE_TEXT,
contact.__class__.CONTACT_PHONE_BOTH]):
logger.debug("Texting {0} at {1}".format(contact.user.username, logger.debug("Texting {0} at {1}".format(contact.user.username,
contact.phone)) contact.phone))
twilio_client.messages.create(to=contact.phone, from_=from_, twilio_client.messages.create(to=contact.phone, from_=from_,
body=msg_body) body=msg_body)
except Exception as e: except Exception as e:
logger.exception("Failed to contact {0} at {1}.".format( logger.exception("Failed to text {0} at {1}.".format(
contact.user.username, contact.phone)) contact.user.username, contact.phone))
from django.conf import settings
from django.db.models import Q
from events.shortcuts import is_event
from search.query.labels import filter_for_labels
from superevents.shortcuts import is_superevent
from .models import Contact, Notification
class CreationRecipientGetter(object):
queryset = Notification.objects.all()
def __init__(self, event_or_superevent, **kwargs):
self.es = event_or_superevent
self.is_event_alert = is_event(event_or_superevent)
self.event = event_or_superevent if self.is_event_alert \
else event_or_superevent.preferred_event
self.process_kwargs(**kwargs)
# event_or_superevent queryset - used for filtering by labels
self.es_qs = self.es._meta.model.objects.filter(pk=self.es.pk)
def process_kwargs(self, **kwargs):
pass
def get_category_filter(self):
if self.is_event_alert:
return Q(category=Notification.NOTIFICATION_CATEGORY_EVENT)
return Q(category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
def get_far_filter(self):
query = Q(far_threshold__isnull=True)
if self.event.far:
query |= Q(far_threshold__gt=self.event.far)
return query
def get_nscand_filter(self):
if self.event.is_ns_candidate():
return Q()
return Q(ns_candidate=False)
def get_group_filter(self):
if self.is_event_alert:
return Q(groups__isnull=True) | Q(groups=self.event.group)
return Q()
def get_pipeline_filter(self):
if self.is_event_alert:
return Q(pipelines__isnull=True) | Q(pipelines=self.event.pipeline)
return Q()
def get_search_filter(self):
if self.is_event_alert:
return Q(searches__isnull=True) | Q(searches=self.event.search)
return Q()
def get_filter_query(self):
filter_list = [getattr(self, method)() for method in dir(self)
if method.startswith('get_') and method.endswith('_filter')]
if filter_list:
return reduce(Q.__and__, filter_list)
return Q()
def get_trigger_query(self):
return Q()
def filter_for_labels(self, notifications):
notification_pks = []
for n in notifications:
if n.labels.exists() and not n.label_query:
if not set(n.labels.all()).issubset(self.es.labels.all()):
continue
elif n.label_query:
qs_out = filter_for_labels(self.es_qs, n.label_query)
if not qs_out.exists():
continue
notification_pks.append(n.pk)
return Notification.objects.filter(pk__in=notification_pks)
def get_contacts_for_notifications(self, notifications):
# Get contacts; make sure contacts are verified and user is in the
# LVC group (safeguards)
contacts = Contact.objects.filter(notification__in=notifications,
verified=True, user__groups__name=settings.LVC_GROUP) \
.select_related('user')
# Separate into email and phone contacts
email_recipients = contacts.filter(email__isnull=False)
phone_recipients = contacts.filter(phone__isnull=False)
return email_recipients, phone_recipients
def get_recipients(self):
# Get trigger query and apply to get baseline set of notifications
trigger_query = self.get_trigger_query()
base_notifications = self.queryset.filter(trigger_query)
# Get and apply filter query to trim it down
filter_query = self.get_filter_query()
notifications = base_notifications.filter(filter_query)
# Do label filtering
final_notifications = self.filter_for_labels(notifications)
# Get email and phone recipients and return
return self.get_contacts_for_notifications(final_notifications)
class UpdateRecipientGetter(CreationRecipientGetter):
def process_kwargs(self, **kwargs):
self.old_far = kwargs.get('old_far', None)
self.old_nscand = kwargs.get('old_nscand', None)
def get_trigger_query(self):
# Initial query should match no objects
query = Q(pk__in=[])
# Then we add other options that could possibly match
if self.event.far is not None:
if self.old_far is None:
query |= Q(far_threshold__gt=self.event.far)
else:
query |= (Q(far_threshold__lte=self.old_far) &
Q(far_threshold__gt=self.event.far))
if not self.old_nscand and self.event.is_ns_candidate():
query |= Q(ns_candidate=True)
return query
class LabelAddedRecipientGetter(CreationRecipientGetter):
def process_kwargs(self, **kwargs):
self.label = kwargs.get('label', None)
if self.label is None:
raise ValueError('label must be provided')
def get_recipients(self):
# Any notification that might be triggered by a label_added action
# should have that label in the 'labels' field. This includes
# notifications with a label_query. Part of the Notification creation
# process picks out all labels in the label_query (even negated ones)
# and adds the to the 'labels' field.
base_notifications = self.label.notification_set.all()
# Get and apply filter query to trim it down
filter_query = self.get_filter_query()
notifications = base_notifications.filter(filter_query)
# Do label filtering
final_notifications = self.filter_for_labels(notifications)
# Get email and phone recipients and return
return self.get_contacts_for_notifications(final_notifications)
class LabelRemovedRecipientGetter(LabelAddedRecipientGetter):
def filter_for_labels(self, notifications):
# Only notifications with a label query should be triggered
# by a label_removed alert, since notifications with a
# label set can only have non-negated labels.
notification_pks = []
for n in notifications:
if n.label_query:
qs_out = filter_for_labels(self.es_qs, n.label_query)
if not qs_out.exists():
continue
notification_pks.append(n.pk)
return Notification.objects.filter(pk__in=notification_pks)
# Dict which maps alert types to recipient getter classes
ALERT_TYPE_RECIPIENT_GETTERS = {
'new': CreationRecipientGetter,
'update': UpdateRecipientGetter,
'label_added': LabelAddedRecipientGetter,
'label_removed': LabelRemovedRecipientGetter,
}
from __future__ import absolute_import from __future__ import absolute_import
from pyparsing import oneOf, Literal, Optional, ZeroOrMore, StringEnd, Suppress from pyparsing import oneOf, Literal, Optional, ZeroOrMore, StringEnd, Suppress
import re
from django.db.models import Q from django.db.models import Q
...@@ -29,3 +30,17 @@ def parse_label_query(s): ...@@ -29,3 +30,17 @@ def parse_label_query(s):
ZeroOrMore(im + label) + StringEnd() ZeroOrMore(im + label) + StringEnd()
return labelQ.parseString(s).asList() return labelQ.parseString(s).asList()
def convert_superevent_id_to_speech(sid):
"""Used for Twilio voice calls"""
grps = list(re.match('^(\w+)(\d{2})(\d{2})(\d{2})(\w+)$', sid).groups())
# Add spaces between all letters in prefix and suffix
grps[0] = " ".join(grps[0])
grps[-1] = " ".join(grps[-1])
# Join with spaces, replace leading zeroes with Os
# and make uppercase
twilio_str = " ".join(grps).replace(' 0', ' O').upper()
return twilio_str
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment