Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • alexander.pace/server
  • geoffrey.mo/gracedb-server
  • deep.chatterjee/gracedb-server
  • cody.messick/server
  • sushant.sharma-chaudhary/server
  • michael-coughlin/server
  • daniel.wysocki/gracedb-server
  • roberto.depietri/gracedb
  • philippe.grassia/gracedb
  • tri.nguyen/gracedb
  • jonah-kanner/gracedb
  • brandon.piotrzkowski/gracedb
  • joseph-areeda/gracedb
  • duncanmmacleod/gracedb
  • thomas.downes/gracedb
  • tanner.prestegard/gracedb
  • leo-singer/gracedb
  • computing/gracedb/server
18 results
Show changes
Showing
with 5739 additions and 0 deletions
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.contrib.auth.models import Group as AuthGroup
from django.urls import reverse
from core.tests.utils import GraceDbTestBase
from alerts.models import Contact, Notification
class TestIndexView(GraceDbTestBase):
"""Test user access to main alerts index page"""
def test_internal_user_get(self):
"""Internal user can get to index view"""
url = reverse('alerts:index')
response = self.request_as_user(url, "GET", self.internal_user)
self.assertEqual(response.status_code, 200)
def test_lvem_user_get(self):
"""LV-EM user can get to index view"""
url = reverse('alerts:index')
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 403)
def test_public_user_get(self):
"""Public user can't get to index view"""
url = reverse('alerts:index')
response = self.request_as_user(url, "GET")
# Should be redirected to login page
self.assertEqual(response.status_code, 403)
class TestContactCreationView(GraceDbTestBase):
"""Test user access to contact creation view"""
def test_internal_user_get(self):
"""Internal user can get contact creation view"""
url = reverse('alerts:create-contact')
response = self.request_as_user(url, "GET", self.internal_user)
self.assertEqual(response.status_code, 200)
def test_lvem_user_get(self):
"""LV-EM user can't get contact creation view"""
url = reverse('alerts:create-contact')
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 403)
def test_public_user_get(self):
"""Public user can't get contact creation view"""
url = reverse('alerts:create-contact')
response = self.request_as_user(url, "GET")
self.assertEqual(response.status_code, 403)
# Prevent test emails from going out
@mock.patch('alerts.views.EmailMessage')
class TestContactTestView(GraceDbTestBase):
"""Test user access to contact testing view"""
@classmethod
def setUpTestData(cls):
super(TestContactTestView, cls).setUpTestData()
# Create a contact
cls.contact = Contact.objects.create(user=cls.internal_user,
description='test contact', email='test@test.com')
def test_internal_user_test(self, mock_email_message):
"""Internal user can test contacts"""
url = reverse('alerts:test-contact', args=[self.contact.id])
response = self.request_as_user(url, "GET", self.internal_user)
# Expect 302 since we are redirected to alerts index page
# after the test
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('alerts:index'))
def test_lvem_user_test(self, mock_email_message):
"""LV-EM user can't test contacts"""
url = reverse('alerts:create-contact')
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 403)
def test_public_user_test(self, mock_email_message):
"""Public user can't test contacts"""
url = reverse('alerts:create-contact')
response = self.request_as_user(url, "GET")
self.assertEqual(response.status_code, 403)
class TestContactDeleteView(GraceDbTestBase):
"""Test user access to contact deletion view"""
@classmethod
def setUpTestData(cls):
super(TestContactDeleteView, cls).setUpTestData()
# Create a contact
cls.contact = Contact.objects.create(user=cls.internal_user,
description='test contact', email='test@test.com')
# Create another contact
cls.other_contact = Contact.objects.create(user=cls.lvem_user,
description='test contact', email='test@test.com')
def test_internal_user_delete(self):
"""Internal user can delete their contacts"""
url = reverse('alerts:delete-contact', args=[self.contact.id])
response = self.request_as_user(url, "GET", self.internal_user)
# Expect 302 since we are redirected to alerts index page
# after the contact is deleted
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('alerts:index'))
# Assert that contact is deleted
with self.assertRaises(self.contact.DoesNotExist):
self.contact.refresh_from_db()
def test_internal_user_delete_other(self):
"""Internal user can't delete other users' contacts"""
url = reverse('alerts:delete-contact',
args=[self.other_contact.id])
response = self.request_as_user(url, "GET", self.internal_user)
self.assertEqual(response.status_code, 404)
def test_lvem_user_get(self):
"""LV-EM user can't delete contacts"""
# Even if an LV-EM user somehow ended up with a contact
# (see self.other_contact), they can't delete it.
for c in Contact.objects.all():
url = reverse('alerts:delete-contact', args=[c.id])
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 403)
def test_public_user_get(self):
"""Public user can't delete contacts"""
for c in Contact.objects.all():
url = reverse('alerts:delete-contact', args=[c.id])
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 403)
class TestNotificationCreateView(GraceDbTestBase):
"""Test user access to notification creation view"""
def test_internal_user_get_with_verified_contact(self):
"""
Internal user can get notification creation view if they have a
verified contact
"""
url = reverse('alerts:create-notification')
self.internal_user.contact_set.create(phone='12345678901',
phone_method=Contact.CONTACT_PHONE_TEXT, verified=True,
description='test')
response = self.request_as_user(url, "GET", self.internal_user)
self.assertEqual(response.status_code, 200)
def test_internal_user_get_with_no_verified_contact(self):
"""
Internal user can't get notification creation view if they don't have
a verified contact
"""
url = reverse('alerts:create-notification')
response = self.request_as_user(url, "GET", self.internal_user)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('alerts:index'))
def test_lvem_user_get(self):
"""LV-EM user can't get notification creation view"""
url = reverse('alerts:create-notification')
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 403)
def test_public_user_get(self):
"""Public user can't get notification creation view"""
url = reverse('alerts:create-notification')
response = self.request_as_user(url, "GET")
# User should be redirected to login view
self.assertEqual(response.status_code, 403)
class TestNotificationDeleteView(GraceDbTestBase):
"""Test user access to notification deletion view"""
@classmethod
def setUpTestData(cls):
super(TestNotificationDeleteView, cls).setUpTestData()
# Create a contact and a notification
cls.contact = Contact.objects.create(user=cls.internal_user,
description='test contact', email='test@test.com')
cls.notification = Notification.objects.create(user=cls.internal_user)
cls.notification.contacts.add(cls.contact)
# Create another contact and notification
# We use lvem_user as the user account just so it can be used for
# testing deletion of another user's contacts. But in reality, an
# LV-EM user should never have a contact or notification
cls.other_contact = Contact.objects.create(user=cls.lvem_user,
description='test contact', email='test@test.com')
cls.other_notification = Notification.objects.create(
user=cls.lvem_user)
cls.other_notification.contacts.add(cls.other_contact)
def test_internal_user_delete(self):
"""Internal user can delete their notifications"""
url = reverse('alerts:delete-notification',
args=[self.notification.id])
response = self.request_as_user(url, "GET", self.internal_user)
# Expect 302 since we are redirected to alerts index page
# after the notification is deleted
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('alerts:index'))
# Assert that contact is deleted
with self.assertRaises(self.notification.DoesNotExist):
self.notification.refresh_from_db()
def test_internal_user_delete_other(self):
"""Internal user can't delete other users' notifications"""
url = reverse('alerts:delete-notification',
args=[self.other_notification.id])
response = self.request_as_user(url, "GET", self.internal_user)
self.assertEqual(response.status_code, 404)
def test_lvem_user_delete(self):
"""LV-EM user can't delete notifications"""
for t in Notification.objects.all():
url = reverse('alerts:delete-notification', args=[t.id])
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 403)
def test_public_user_delete(self):
"""Public user can't delete notifications"""
for t in Notification.objects.all():
url = reverse('alerts:delete-notification', args=[t.id])
response = self.request_as_user(url, "GET")
self.assertEqual(response.status_code, 403)
import itertools
try:
from unittest import mock
except ImportError: # python < 3
import mock
import types
import pytest
from alerts.main import issue_alerts
from events.models import Event
from superevents.models import Superevent
###############################################################################
# FIXTURES ####################################################################
###############################################################################
# Mock recipient getter dict
@pytest.fixture
def mock_rg_dict():
def _inner(alert_type='new', recips_exist=True):
# Set up mock recipient getter stuff
mock_recipients = mock.MagicMock()
mock_recipients.exists.return_value = recips_exist
mock_rg = mock.MagicMock()
mock_rg.get_recipients.return_value = \
(mock_recipients, mock_recipients)
mock_rg_class = mock.MagicMock()
mock_rg_class.return_value = mock_rg
mock_rg_dict = {alert_type: mock_rg_class}
return mock_rg_dict
return _inner
###############################################################################
# TESTS #######################################################################
###############################################################################
@pytest.mark.parametrize(
"xmpp,email,phone",
list(itertools.product((True, False), repeat=3))
)
def test_alert_settings(settings, mock_rg_dict, xmpp, email, phone):
# Set up settings
settings.SEND_XMPP_ALERTS = xmpp
settings.SEND_EMAIL_ALERTS = email
settings.SEND_PHONE_ALERTS = phone
# Set up mock superevent object
superevent = mock.MagicMock()
superevent.is_test.return_value = False
superevent.is_mdc.return_value = False
preferred_event = mock.MagicMock()
type(preferred_event).offline = mock.PropertyMock(return_value=False)
superevent.preferred_event = preferred_event
# Call issue_alerts
with mock.patch('alerts.main.issue_xmpp_alerts') as mock_xmpp, \
mock.patch('alerts.main.issue_email_alerts') as mock_email, \
mock.patch('alerts.main.issue_phone_alerts') as mock_phone, \
mock.patch('alerts.main.is_event') as mock_is_event, \
mock.patch.dict('alerts.main.ALERT_TYPE_RECIPIENT_GETTERS',
mock_rg_dict(), clear=True):
mock_is_event.return_value = False
issue_alerts(superevent, 'new', None)
# Check results
if xmpp:
assert mock_xmpp.call_count == 1
if phone:
assert mock_phone.call_count == 1
if email:
assert mock_email.call_count == 1
if not (phone or email):
assert mock_is_event.call_count == 0
@pytest.mark.parametrize(
"is_event,is_mdc,is_test,is_offline",
list(itertools.product((True, False), repeat=4))
)
def test_no_alerts_for_test_mdc_offline_events_and_superevents(
settings, mock_rg_dict, is_event, is_mdc, is_test, is_offline
):
# Set up settings
settings.SEND_XMPP_ALERTS = False
settings.SEND_EMAIL_ALERTS = True
settings.SEND_PHONE_ALERTS = True
# Set up mock event/superevent object
# First, set up event
event = mock.MagicMock()
type(event).offline = mock.PropertyMock(return_value=is_offline)
# If we're doing a superevent, then set the event as its preferred_event
if not is_event:
es = mock.MagicMock()
es.preferred_event = event
else:
es = event
# is_mdc and is_test are handled the same way for both events and
# superevents, so we can set it up at this point
es.is_mdc.return_value = is_mdc
es.is_test.return_value = is_test
# Instantiate mock_rg_dict
mock_rg_dict = mock_rg_dict()
# Call issue_alerts
with mock.patch('alerts.main.issue_xmpp_alerts') as mock_xmpp, \
mock.patch('alerts.main.issue_email_alerts') as mock_email, \
mock.patch('alerts.main.issue_phone_alerts') as mock_phone, \
mock.patch('alerts.main.is_event') as mock_is_event, \
mock.patch.dict('alerts.main.ALERT_TYPE_RECIPIENT_GETTERS',
mock_rg_dict, clear=True):
mock_is_event.return_value = is_event
issue_alerts(es, 'new', None)
# Check results
assert es.is_mdc.call_count == 1
assert es.is_test.call_count == int(not es.is_mdc())
# Whether the recipient_getter class is called or not depends
# finally on whether the event is offline or not
if (not (es.is_mdc() or es.is_test())):
assert mock_rg_dict['new'].call_count == int(not event.offline)
else:
assert mock_rg_dict['new'].call_count == 0
import pytest
from django.contrib.auth import get_user_model
from alerts.models import Contact, Notification
from alerts.recipients import CreationRecipientGetter
UserModel = get_user_model()
@pytest.mark.django_db
def test_multiple_contacts(superevent, internal_user):
# Set up contacts and notifications
c1 = Contact.objects.create(
user=internal_user, description='c1', verified=True,
phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
)
c2 = Contact.objects.create(
user=internal_user, description='c2', verified=True,
email='test@test.com'
)
n = Notification.objects.create(
user=internal_user, description='test',
category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
)
n.contacts.add(*(c1, c2))
# Get recipients
recipient_getter = CreationRecipientGetter(superevent)
email_contacts, phone_contacts = recipient_getter.get_recipients()
# Check results
assert email_contacts.count() == 1
assert email_contacts.first().pk == c2.pk
assert phone_contacts.count() == 1
assert phone_contacts.first().pk == c1.pk
@pytest.mark.django_db
def test_duplicate_contacts(superevent, internal_user):
# Set up contacts and notifications
c = Contact.objects.create(
user=internal_user, description='test', verified=True,
phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
)
n1 = Notification.objects.create(
user=internal_user, description='n1',
category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
)
n2 = Notification.objects.create(
user=internal_user, description='n2',
category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
)
n1.contacts.add(c)
n2.contacts.add(c)
# Get notifications and check results
recipient_getter = CreationRecipientGetter(superevent)
notifications = recipient_getter.get_notifications()
assert notifications.count() == 2
for pk in (n1.pk, n2.pk):
assert notifications.filter(pk=pk).exists()
# Get recipients and check results
email_contacts, phone_contacts = recipient_getter.get_recipients()
# Check results
assert email_contacts.count() == 0
assert phone_contacts.count() == 1
assert phone_contacts.first().pk == c.pk
@pytest.mark.django_db
def test_contacts_non_internal_user(superevent, internal_user):
# NOTE: this test handles the case where a user with contacts/notifications
# already set up leaves the collboration.
# Create a non-internal user
external_user = UserModel.objects.create(username='external.user')
# Set up contacts and notifications
c_internal = Contact.objects.create(
user=internal_user, description='test', verified=True,
phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
)
c_external = Contact.objects.create(
user=external_user, description='test', verified=True,
phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
)
n_internal = Notification.objects.create(
user=internal_user, description='internal',
category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
)
n_internal.contacts.add(c_internal)
n_external = Notification.objects.create(
user=external_user, description='external',
category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
)
n_external.contacts.add(c_external)
# Get notifications and check results
recipient_getter = CreationRecipientGetter(superevent)
notifications = recipient_getter.get_notifications()
assert notifications.count() == 2
for pk in (n_internal.pk, n_external.pk):
assert notifications.filter(pk=pk).exists()
# Get recipients and check results
email_contacts, phone_contacts = recipient_getter.get_recipients()
# Check results
assert email_contacts.count() == 0
assert phone_contacts.count() == 1
assert phone_contacts.first().pk == c_internal.pk
@pytest.mark.django_db
def test_unverified_contact(superevent, internal_user):
# NOTE: this test handles the case where a user with contacts/notifications
# already set up leaves the collboration.
# Set up contacts and notifications
c_verified = Contact.objects.create(
user=internal_user, description='test', verified=True,
phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
)
c_unverified = Contact.objects.create(
user=internal_user, description='test', verified=False,
phone='12345678901', phone_method=Contact.CONTACT_PHONE_TEXT,
)
n = Notification.objects.create(
user=internal_user, description='internal',
category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT
)
n.contacts.add(*(c_verified, c_unverified))
# Get notifications and check results
recipient_getter = CreationRecipientGetter(superevent)
notifications = recipient_getter.get_notifications()
assert notifications.count() == 1
assert notifications.first().pk == n.pk
# Get recipients and check results
email_contacts, phone_contacts = recipient_getter.get_recipients()
# Check results
assert email_contacts.count() == 0
assert phone_contacts.count() == 1
assert phone_contacts.first().pk == c_verified.pk
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.test import override_settings
from alerts.models import Contact, Notification
from alerts.email import issue_email_alerts, prepare_email_body
from core.tests.utils import GraceDbTestBase
from core.time_utils import gpsToUtc
from events.models import Label
from superevents.tests.mixins import SupereventCreateMixin
class TestEmailBody(GraceDbTestBase, SupereventCreateMixin):
@classmethod
def setUpTestData(cls):
super(TestEmailBody, cls).setUpTestData()
# Create a superevent
cls.superevent = cls.create_superevent(cls.internal_user,
'fake_group', 'fake_pipeline', 'fake_search')
# References to cls/self.event will refer to the superevent's
cls.event = cls.superevent.preferred_event
# Create a few labels
cls.label1, _ = Label.objects.get_or_create(name='TEST_LABEL1')
cls.label2, _ = Label.objects.get_or_create(name='TEST_LABEL2')
def test_new_event(self):
"""Test email contents for vanilla new event"""
# Get email body
email_body = prepare_email_body(self.event, 'new')
# Check email body
lines = email_body.split('\n')
self.assertIn('New {grp} / {pipeline} event: {gid}'.format(
grp=self.event.group.name, pipeline=self.event.pipeline.name,
gid=self.event.graceid), lines[0])
self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
lines[2])
self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
self.event.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.event.labels.values_list('name', flat=True))),
lines[7])
def test_new_event_more_info(self):
"""Test email contents for more interesting new event"""
# Make event more interesting
self.event.labelling_set.create(label=self.label1,
creator=self.internal_user)
self.event.labelling_set.create(label=self.label2,
creator=self.internal_user)
self.event.far = 1e-10
self.event.singleinspiral_set.create(mass1=3, mass2=1)
self.event.instruments = 'I1,I2'
self.event.save()
# Get email body
email_body = prepare_email_body(self.event, 'new')
# Check email body
lines = email_body.split('\n')
self.assertIn('New {grp} / {pipeline} event: {gid}'.format(
grp=self.event.group.name, pipeline=self.event.pipeline.name,
gid=self.event.graceid), lines[0])
self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
lines[2])
self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
self.event.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.event.labels.values_list('name', flat=True))),
lines[7])
self.assertIn('Instruments: {inst}'.format(inst=
self.event.instruments), lines[8])
self.assertIn('Component masses: {m1}, {m2}'.format(
m1=self.event.singleinspiral_set.first().mass1,
m2=self.event.singleinspiral_set.first().mass2), lines[9])
def test_update_event(self):
"""Test email contents for event update"""
# Get email body
email_body = prepare_email_body(self.event, 'update')
# Check email body
lines = email_body.split('\n')
self.assertIn('Updated {grp} / {pipeline} event: {gid}'.format(
grp=self.event.group.name, pipeline=self.event.pipeline.name,
gid=self.event.graceid), lines[0])
self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
lines[2])
self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
self.event.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.event.labels.values_list('name', flat=True))),
lines[7])
def test_label_added_event(self):
"""Test email contents for label_added to event alert"""
# Get email body
email_body = prepare_email_body(self.event, 'label_added',
label=self.label1)
# Check email body
lines = email_body.split('\n')
self.assertIn(('Label {label} added to {grp} / {pipeline} event: '
'{gid}').format(label=self.label1.name, grp=self.event.group.name,
pipeline=self.event.pipeline.name, gid=self.event.graceid),
lines[0])
self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
lines[2])
self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
self.event.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.event.labels.values_list('name', flat=True))),
lines[7])
def test_label_removed_event(self):
"""Test email contents for label_removed to event alert"""
# Get email body
email_body = prepare_email_body(self.event, 'label_removed',
label=self.label1)
# Check email body
lines = email_body.split('\n')
self.assertIn(('Label {label} removed from {grp} / {pipeline} event: '
'{gid}').format(label=self.label1.name, grp=self.event.group.name,
pipeline=self.event.pipeline.name, gid=self.event.graceid),
lines[0])
self.assertIn('Event time (GPS): {gps}'.format(gps=self.event.gpstime),
lines[2])
self.assertIn('Event time (UTC): {utc}'.format(utc=gpsToUtc(
self.event.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.event.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.event.labels.values_list('name', flat=True))),
lines[7])
def test_new_superevent(self):
"""Test email contents for vanilla new superevent"""
# Get email body
email_body = prepare_email_body(self.superevent, 'new')
# Check email body
lines = email_body.split('\n')
self.assertIn('New superevent: {sid}'.format(
sid=self.superevent.superevent_id), lines[0])
self.assertIn('Superevent time (GPS): {gps}'.format(
gps=self.superevent.gpstime), lines[2])
self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
self.superevent.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.superevent.labels.values_list('name', flat=True))),
lines[7])
def test_new_superevent_more_info(self):
"""Test email contents for more interesting new event"""
# Make superevent more interesting
self.superevent.labelling_set.create(label=self.label1,
creator=self.internal_user)
self.superevent.labelling_set.create(label=self.label2,
creator=self.internal_user)
self.event.far = 1e-10
self.event.singleinspiral_set.create(mass1=3, mass2=1)
self.event.save()
# Get email body
email_body = prepare_email_body(self.superevent, 'new')
# Check email body
lines = email_body.split('\n')
self.assertIn('New superevent: {sid}'.format(
sid=self.superevent.superevent_id), lines[0])
self.assertIn('Superevent time (GPS): {gps}'.format(
gps=self.superevent.gpstime), lines[2])
self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
self.superevent.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.superevent.labels.values_list('name', flat=True))),
lines[7])
self.assertIn('Component masses: {m1}, {m2}'.format(
m1=self.superevent.preferred_event.singleinspiral_set.first().mass1,
m2=self.superevent.preferred_event.singleinspiral_set.first().mass2),
lines[8])
def test_update_superevent(self):
"""Test email contents for superevent update"""
# Get email body
email_body = prepare_email_body(self.superevent, 'update')
# Check email body
lines = email_body.split('\n')
self.assertIn('Updated superevent: {sid}'.format(
sid=self.superevent.superevent_id), lines[0])
self.assertIn('Superevent time (GPS): {gps}'.format(
gps=self.superevent.gpstime), lines[2])
self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
self.superevent.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.superevent.labels.values_list('name', flat=True))),
lines[7])
def test_label_added_superevent(self):
"""Test email contents for label_added to superevent alert"""
# Get email body
email_body = prepare_email_body(self.superevent, 'label_added',
label=self.label1)
# Check email body
lines = email_body.split('\n')
self.assertIn('Label {label} added to superevent: {sid}'.format(
label=self.label1.name, sid=self.superevent.superevent_id),
lines[0])
self.assertIn('Superevent time (GPS): {gps}'.format(
gps=self.superevent.gpstime), lines[2])
self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
self.superevent.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.superevent.labels.values_list('name', flat=True))),
lines[7])
def test_label_removed_superevent(self):
"""Test email contents for label_removed from superevent alert"""
# Get email body
email_body = prepare_email_body(self.superevent, 'label_removed',
label=self.label1)
# Check email body
lines = email_body.split('\n')
self.assertIn('Label {label} removed from superevent: {sid}'.format(
label=self.label1.name, sid=self.superevent.superevent_id),
lines[0])
self.assertIn('Superevent time (GPS): {gps}'.format(
gps=self.superevent.gpstime), lines[2])
self.assertIn('Superevent time (UTC): {utc}'.format(utc=gpsToUtc(
self.superevent.gpstime).isoformat()), lines[3])
self.assertIn('FAR: {far}'.format(far=self.superevent.far), lines[6])
self.assertIn('Labels: {labels}'.format(labels=", ".join(
self.superevent.labels.values_list('name', flat=True))),
lines[7])
@pytest.mark.skip(reason='This needs to be updated for EGAD')
@mock.patch('alerts.email.EmailMessage')
class TestEmailSend(GraceDbTestBase, SupereventCreateMixin):
@classmethod
def setUpTestData(cls):
super(TestEmailSend, cls).setUpTestData()
# Create a superevent
cls.superevent = cls.create_superevent(cls.internal_user,
'fake_group', 'fake_pipeline', 'fake_search')
# References to cls/self.event will refer to the superevent's
cls.event = cls.superevent.preferred_event
# Create a few labels
cls.label1, _ = Label.objects.get_or_create(name='TEST_LABEL1')
cls.label2, _ = Label.objects.get_or_create(name='TEST_LABEL2')
# Create a few email contacts
cls.contact1 = Contact.objects.create(user=cls.internal_user,
description='contact1', email='contact1@test.com',
verified=True)
cls.contact2 = Contact.objects.create(user=cls.internal_user,
description='contact2', email='contact2@test.com',
verified=True)
cls.contacts = Contact.objects.filter(pk__in=[cls.contact1.pk,
cls.contact2.pk])
def test_new_superevent(self, mock_email):
"""Test contacts and email subject for new superevent alert"""
issue_email_alerts(self.superevent, 'new', self.contacts)
# Check calls to EmailMessage init
self.assertEqual(mock_email.call_count, self.contacts.count())
# Check 'to' address
tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
'email', flat=True))))
# Check subject
subject = mock_email.call_args[1]['subject']
self.assertEqual(subject, '[gracedb] Superevent created: {sid}'
.format(sid=self.superevent.superevent_id))
def test_update_superevent(self, mock_email):
"""Test contacts and email subject for update superevent alert"""
issue_email_alerts(self.superevent, 'update', self.contacts)
# Check calls to EmailMessage init
self.assertEqual(mock_email.call_count, self.contacts.count())
# Check 'to' address
tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
'email', flat=True))))
# Check subject
subject = mock_email.call_args[1]['subject']
self.assertEqual(subject, '[gracedb] Superevent updated: {sid}'
.format(sid=self.superevent.superevent_id))
def test_label_added_superevent(self, mock_email):
"""Test contacts and email subject for label_added superevent alert"""
issue_email_alerts(self.superevent, 'label_added', self.contacts,
label=self.label1)
# Check calls to EmailMessage init
self.assertEqual(mock_email.call_count, self.contacts.count())
# Check 'to' address
tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
'email', flat=True))))
# Check subject
subject = mock_email.call_args[1]['subject']
self.assertEqual(subject, ('[gracedb] Superevent labeled with '
'{label}: {sid}').format(label=self.label1.name,
sid=self.superevent.superevent_id))
def test_label_removed_superevent(self, mock_email):
"""
Test contacts and email subject for label_removed superevent alert
"""
issue_email_alerts(self.superevent, 'label_removed', self.contacts,
label=self.label1)
# Check calls to EmailMessage init
self.assertEqual(mock_email.call_count, self.contacts.count())
# Check 'to' address
tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
'email', flat=True))))
# Check subject
subject = mock_email.call_args[1]['subject']
self.assertEqual(subject, ('[gracedb] Label {label} removed from '
'superevent: {sid}').format(label=self.label1.name,
sid=self.superevent.superevent_id))
def test_new_event(self, mock_email):
"""Test contacts and email subject for new event alert"""
issue_email_alerts(self.event, 'new', self.contacts)
# Check calls to EmailMessage init
self.assertEqual(mock_email.call_count, self.contacts.count())
# Check 'to' address
tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
'email', flat=True))))
# Check subject
subject = mock_email.call_args[1]['subject']
self.assertEqual(subject, '[gracedb] {pipeline} event created: {gid}'
.format(pipeline=self.event.pipeline.name, gid=self.event.graceid))
def test_update_event(self, mock_email):
"""Test contacts and email subject for update event alert"""
issue_email_alerts(self.event, 'update', self.contacts)
# Check calls to EmailMessage init
self.assertEqual(mock_email.call_count, self.contacts.count())
# Check 'to' address
tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
'email', flat=True))))
# Check subject
subject = mock_email.call_args[1]['subject']
self.assertEqual(subject, '[gracedb] {pipeline} event updated: {gid}'
.format(pipeline=self.event.pipeline.name, gid=self.event.graceid))
def test_label_added_superevent(self, mock_email):
"""Test contacts and email subject for label_added event alert"""
issue_email_alerts(self.event, 'label_added', self.contacts,
label=self.label1)
# Check calls to EmailMessage init
self.assertEqual(mock_email.call_count, self.contacts.count())
# Check 'to' address
tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
'email', flat=True))))
# Check subject
subject = mock_email.call_args[1]['subject']
self.assertEqual(subject, ('[gracedb] {pipeline} event labeled with '
'{label}: {gid}').format(pipeline=self.event.pipeline.name,
label=self.label1.name, gid=self.event.graceid))
def test_label_removed_event(self, mock_email):
"""Test contacts and email subject for label_removed event alert"""
issue_email_alerts(self.event, 'label_removed', self.contacts,
label=self.label1)
# Check calls to EmailMessage init
self.assertEqual(mock_email.call_count, self.contacts.count())
# Check 'to' address
tos = [ca[1]['to'][0] for ca in mock_email.call_args_list]
self.assertEqual(sorted(tos), sorted(list(self.contacts.values_list(
'email', flat=True))))
# Check subject
subject = mock_email.call_args[1]['subject']
self.assertEqual(subject, ('[gracedb] Label {label} removed from '
'{pipeline} event: {gid}').format(label=self.label1.name,
pipeline=self.event.pipeline.name, gid=self.event.graceid))
try:
from unittest import mock
except ImportError: # python < 3
import mock
import string
import pytest
import itertools
from alerts.constants import AND, OR, NOT
from alerts.utils import get_label_query_parser, parse_label_query
from events.models import Label
# NOTE: list('ABC') == ['A', 'B', 'C']
@pytest.mark.parametrize(
"labels,label_query,match",
[
(['A'], 'A & B', False),
(['A', 'B'], 'A & B', True),
(['A', 'B'], '~B', False),
(['A', 'B'], '~B', False),
(['A', 'B'], '~B', False),
(list('ABC'), 'A & B', True),
(['C'], 'A & B | C', True),
(['C'], '~C | B', False),
(list('ABCD'), 'A & B & C & D', True),
(list('ABCD'), 'A & B & C & D & ~E', True),
(list('ABCD'), 'A & B & C & D & E', False),
(list('ABCD'), 'A & C | C & ~E', True),
(list('ABCD'), '~A | C & ~E', True),
(list('ABCD'), '~A | D & E', False),
(list('ABCD'), 'A', True),
(list('ABCD'), 'A | F | G | H', True),
(list('ABCD'), 'E | F | C | H', True),
(list('ABCD'), 'Y | F | Z | H', False),
(list('ABCD'), 'A & B | ~C & ~D', True),
# Try a few with werid spacings
(list('ABCD'), 'A|D &~E', True),
(['A', 'B'], '~A |B&C&~D', False),
(['A', 'B'], 'A| B|C&~D', True),
]
)
def test_label_query_parsing(labels, label_query, match):
label_list_method = 'alerts.utils.Label.objects.values_list'
#with mock.patch('alerts.utils.get_label_parser') as mock_glp:
with mock.patch(label_list_method) as mock_llm:
mock_llm.return_value = list(string.ascii_uppercase)
lqp = get_label_query_parser(labels)
assert lqp.parseString(label_query)[0] == match
PARSE_LABEL_QUERY_INPUTS_OUTPUTS = [
('{A} & {B}', False, ['{A}', '{B}']),
('{A} & {B}', True, ['{A}', '&', '{B}']),
]
@pytest.mark.parametrize(
"query,keep_binary_ops,output",
PARSE_LABEL_QUERY_INPUTS_OUTPUTS,
)
@pytest.mark.django_db
def test_parse_label_query(query, keep_binary_ops, output):
labels = Label.objects.values_list('name', flat=True)
for A_label, B_label in itertools.product(labels, repeat=2):
query_labeled = query.format(A=A_label, B=B_label)
output_labeled = [elem.format(A=A_label, B=B_label) for elem in output]
test_output_labeled = parse_label_query(query_labeled,
keep_binary_ops=keep_binary_ops)
assert output_labeled == test_output_labeled
@pytest.mark.parametrize("num_labels", range(1, 5))
@pytest.mark.django_db
def test_parse_label_query(num_labels):
"""Test many possible inputs to `parse_label_query`"""
# Test with/without whitespace separators.
separator_choices = ['', ' ', '\t']
# Replace DB's Label set with `num_labels` unique values, as the
# allowed labels shouldn't matter.
labels = replace_label_set(num_labels)
# Test all possible combinations of binary and unary operators.
binary_operator_choices = itertools.product(AND+OR, repeat=num_labels-1)
unary_operator_choices = itertools.product(NOT + [''], repeat=num_labels)
operator_choices = itertools.product(binary_operator_choices,
unary_operator_choices)
for binary_operators, unary_operators in operator_choices:
# Expected output if operators are kept
# NOTE: this is also how we construct the query
expected_output_keep_ops = [
token
for token in roundrobin(unary_operators, labels, binary_operators)
if token != '' # filter out empty unary operators
]
# Expected output if operators are not kept
expected_output_no_keep_ops = labels
# # NOTE: This is what would be expected if `keep_binary_ops` only acted
# # on the _binary_ operators, and left the unary operators alone.
# expected_output_no_keep_ops = [
# token
# for token in roundrobin(unary_operators, labels)
# if token != '' # filter out empty unary operators
# ]
# Test all allowed separators
for separator in separator_choices:
# Construct the query from tokens
query = separator.join(expected_output_keep_ops)
# Get the actual output with/without operators kept
actual_output_no_keep_ops = parse_label_query(query,
keep_binary_ops=False)
actual_output_keep_ops = parse_label_query(query,
keep_binary_ops=True)
# Assert that the output is what's expected
assert expected_output_no_keep_ops == actual_output_no_keep_ops
assert expected_output_keep_ops == actual_output_keep_ops
def replace_label_set(num_labels):
# Delete existing labels
Label.objects.all().delete()
# Create a new set of labels
names = []
for i in range(num_labels):
name = f'label{i}'
description = f'Fake label number {i}'
Label.objects.create(name=name, description=description)
names.append(name)
# Return the new labels' names
return names
def roundrobin(*iterables):
"roundrobin('ABC', 'D', 'EF') --> A D E B F C"
# Recipe credited to George Sakkis
num_active = len(iterables)
nexts = itertools.cycle(iter(it).__next__ for it in iterables)
while num_active:
try:
for next in nexts:
yield next()
except StopIteration:
# Remove the iterator we just exhausted from the cycle.
num_active -= 1
nexts = itertools.cycle(itertools.islice(nexts, num_active))
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.test import override_settings
from django.urls import reverse
from django.utils.http import urlencode
from alerts.models import Contact
from alerts.phone import (
get_message_content, compile_twiml_url, issue_phone_alerts,
)
from alerts.utils import convert_superevent_id_to_speech
from core.tests.utils import GraceDbTestBase
from core.time_utils import gpsToUtc
from core.urls import build_absolute_uri
from events.models import Label
from superevents.tests.mixins import SupereventCreateMixin
class TestMessageContent(GraceDbTestBase, SupereventCreateMixin):
"""Test content sent in text message alerts"""
@classmethod
def setUpTestData(cls):
super(TestMessageContent, cls).setUpTestData()
# Create a superevent
cls.superevent = cls.create_superevent(cls.internal_user,
'fake_group', 'fake_pipeline', 'fake_search')
# References to cls/self.event will refer to the superevent's
cls.event = cls.superevent.preferred_event
# Create a label
cls.label, _ = Label.objects.get_or_create(name='TEST_LABEL1')
def test_new_event(self):
"""Test message contents for new event alert"""
message = get_message_content(self.event, 'new')
# Check content
expected = 'A {pipeline} event with GraceDB ID {gid} was created' \
.format(pipeline=self.event.pipeline.name, gid=self.event.graceid)
expected_url = build_absolute_uri(reverse('view',
args=[self.event.graceid]))
self.assertIn(expected, message)
self.assertIn(expected_url, message)
def test_update_event(self):
"""Test message contents for event update alert"""
message = get_message_content(self.event, 'update')
# Check content
expected = 'A {pipeline} event with GraceDB ID {gid} was updated' \
.format(pipeline=self.event.pipeline.name, gid=self.event.graceid)
expected_url = build_absolute_uri(reverse('view',
args=[self.event.graceid]))
self.assertIn(expected, message)
self.assertIn(expected_url, message)
def test_label_added_event(self):
"""Test message contents for label_added event alert"""
message = get_message_content(self.event, 'label_added',
label=self.label)
# Check content
expected = ('A {pipeline} event with GraceDB ID {gid} was labeled '
'with {label}').format(pipeline=self.event.pipeline.name,
label=self.label.name, gid=self.event.graceid)
expected_url = build_absolute_uri(reverse('view',
args=[self.event.graceid]))
self.assertIn(expected, message)
self.assertIn(expected_url, message)
def test_label_removed_event(self):
"""Test message contents for label_removed event alert"""
message = get_message_content(self.event, 'label_removed',
label=self.label)
# Check content
expected = ('The label {label} was removed from a {pipeline} event '
'with GraceDB ID {gid}').format(pipeline=self.event.pipeline.name,
label=self.label.name, gid=self.event.graceid)
expected_url = build_absolute_uri(reverse('view',
args=[self.event.graceid]))
self.assertIn(expected, message)
self.assertIn(expected_url, message)
def test_new_superevent(self):
"""Test message contents for new superevent alert"""
message = get_message_content(self.superevent, 'new')
# Check content
expected = 'A superevent with GraceDB ID {sid} was created'.format(
sid=self.superevent.superevent_id)
expected_url = build_absolute_uri(reverse('superevents:view',
args=[self.superevent.superevent_id]))
self.assertIn(expected, message)
self.assertIn(expected_url, message)
def test_update_superevent(self):
"""Test message contents for superevent update alert"""
message = get_message_content(self.superevent, 'update')
# Check content
expected = 'A superevent with GraceDB ID {sid} was updated'.format(
sid=self.superevent.superevent_id)
expected_url = build_absolute_uri(reverse('superevents:view',
args=[self.superevent.superevent_id]))
self.assertIn(expected, message)
self.assertIn(expected_url, message)
def test_label_added_superevent(self):
"""Test message contents for label_added superevent alert"""
message = get_message_content(self.superevent, 'label_added',
label=self.label)
# Check content
expected = ('A superevent with GraceDB ID {sid} was labeled '
'with {label}').format(sid=self.superevent.superevent_id,
label=self.label.name)
expected_url = build_absolute_uri(reverse('superevents:view',
args=[self.superevent.superevent_id]))
self.assertIn(expected, message)
self.assertIn(expected_url, message)
def test_label_removed_superevent(self):
"""Test message contents for label_removed superevent alert"""
message = get_message_content(self.superevent, 'label_removed',
label=self.label)
# Check content
expected = ('The label {label} was removed from a superevent with '
'GraceDB ID {sid}').format(sid=self.superevent.superevent_id,
label=self.label.name)
expected_url = build_absolute_uri(reverse('superevents:view',
args=[self.superevent.superevent_id]))
self.assertIn(expected, message)
self.assertIn(expected_url, message)
class TestTwimlUrl(GraceDbTestBase, SupereventCreateMixin):
"""Test content sent in text message alerts"""
@classmethod
def setUpTestData(cls):
super(TestTwimlUrl, cls).setUpTestData()
# Create a superevent
cls.superevent = cls.create_superevent(cls.internal_user,
'fake_group', 'fake_pipeline', 'fake_search')
# References to cls/self.event will refer to the superevent's
cls.event = cls.superevent.preferred_event
# Create a label
cls.label, _ = Label.objects.get_or_create(name='TEST_LABEL1')
def test_new_event(self):
"""Test TwiML URL for new event alert"""
url = compile_twiml_url(self.event, 'new')
# Compile expected URL
urlparams = {
'graceid': self.event.graceid,
'pipeline': self.event.pipeline.name,
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['event']['new'],
params=urlencode(urlparams))
# Check URL
self.assertEqual(expected_url, url)
def test_update_event(self):
"""Test TwiML URL for event update alert"""
url = compile_twiml_url(self.event, 'update')
# Compile expected URL
urlparams = {
'graceid': self.event.graceid,
'pipeline': self.event.pipeline.name,
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['event']['update'],
params=urlencode(urlparams))
# Check URL
self.assertEqual(expected_url, url)
def test_label_added_event(self):
"""Test TwiML URL for label_added event alert"""
url = compile_twiml_url(self.event, 'label_added',
label=self.label.name)
# Compile expected URL
urlparams = {
'graceid': self.event.graceid,
'pipeline': self.event.pipeline.name,
'label_lower': self.label.name.lower(),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['event']['label_added'],
params=urlencode(urlparams))
# Check URL
self.assertEqual(expected_url, url)
def test_label_removed_event(self):
"""Test TwiML URL for label_removed event alert"""
url = compile_twiml_url(self.event, 'label_removed',
label=self.label.name)
# Compile expected URL
urlparams = {
'graceid': self.event.graceid,
'pipeline': self.event.pipeline.name,
'label_lower': self.label.name.lower(),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['event']['label_removed'],
params=urlencode(urlparams))
# Check URL
self.assertEqual(expected_url, url)
def test_new_superevent(self):
"""Test TwiML URL for new superevent alert"""
url = compile_twiml_url(self.superevent, 'new')
# Compile expected URL
urlparams = {
'sid': convert_superevent_id_to_speech(
self.superevent.superevent_id),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['superevent']['new'],
params=urlencode(urlparams))
# Check URL
self.assertEqual(expected_url, url)
def test_update_superevent(self):
"""Test TwiML URL for superevent update alert"""
url = compile_twiml_url(self.superevent, 'update')
# Compile expected URL
urlparams = {
'sid': convert_superevent_id_to_speech(
self.superevent.superevent_id),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['superevent']['update'],
params=urlencode(urlparams))
# Check URL
self.assertEqual(expected_url, url)
def test_label_added_superevent(self):
"""Test TwiML URL for label_added superevent alert"""
url = compile_twiml_url(self.superevent, 'label_added',
label=self.label.name)
# Compile expected URL
urlparams = {
'sid': convert_superevent_id_to_speech(
self.superevent.superevent_id),
'label_lower': self.label.name.lower(),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['superevent']['label_added'],
params=urlencode(urlparams))
# Check URL
self.assertEqual(expected_url, url)
def test_label_removed_superevent(self):
"""Test TwiML URL for label_removed superevent alert"""
url = compile_twiml_url(self.superevent, 'label_removed',
label=self.label.name)
# Compile expected URL
urlparams = {
'sid': convert_superevent_id_to_speech(
self.superevent.superevent_id),
'label_lower': self.label.name.lower(),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['superevent']['label_removed'],
params=urlencode(urlparams))
# Check URL
self.assertEqual(expected_url, url)
@pytest.mark.skip(reason='This needs to be updated for EGAD')
@mock.patch('alerts.phone.twilio_client.calls.create')
@mock.patch('alerts.phone.twilio_client.incoming_phone_numbers.list',
lambda: [mock.Mock()])
class TestPhoneCallAndText(GraceDbTestBase, SupereventCreateMixin):
@classmethod
def setUpTestData(cls):
super(TestPhoneCallAndText, cls).setUpTestData()
# Create a superevent
cls.superevent = cls.create_superevent(cls.internal_user,
'fake_group', 'fake_pipeline', 'fake_search')
# References to cls/self.event will refer to the superevent's
cls.event = cls.superevent.preferred_event
# Create a label
cls.label, _ = Label.objects.get_or_create(name='TEST_LABEL1')
# Create a few phone contacts
cls.contact1 = Contact.objects.create(user=cls.internal_user,
description='contact1', phone='12345678901', verified=True,
phone_method=Contact.CONTACT_PHONE_CALL)
cls.contact2 = Contact.objects.create(user=cls.internal_user,
description='contact2', phone='12345654321', verified=True,
phone_method=Contact.CONTACT_PHONE_BOTH)
cls.contacts = Contact.objects.filter(pk__in=[cls.contact1.pk,
cls.contact2.pk])
# Refresh from DB to pull formatted phone numbers
cls.contact1.refresh_from_db()
cls.contact2.refresh_from_db()
def test_new_superevent(self, mock_call, mock_text):
"""Test issuing phone alerts for new superevent alert"""
issue_phone_alerts(self.superevent, 'new', self.contacts)
# Check calls
self.assertEqual(mock_call.call_count, 2)
self.assertEqual(mock_call.call_args_list[0][1]['to'],
self.contact1.phone)
self.assertEqual(mock_call.call_args_list[1][1]['to'],
self.contact2.phone)
# Check texts
self.assertEqual(mock_text.call_count, 1)
self.assertEqual(mock_text.call_args[1]['to'],
self.contact2.phone)
# Check URLs
urlparams = {
'sid': convert_superevent_id_to_speech(
self.superevent.superevent_id),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['superevent']['new'],
params=urlencode(urlparams))
# Check URL
for call_args in mock_call.call_args_list:
self.assertEqual(call_args[1]['url'], expected_url)
def test_update_superevent(self, mock_call, mock_text):
"""Test issuing phone alerts for superevent update alert"""
issue_phone_alerts(self.superevent, 'update', self.contacts)
# Check calls
self.assertEqual(mock_call.call_count, 2)
self.assertEqual(mock_call.call_args_list[0][1]['to'],
self.contact1.phone)
self.assertEqual(mock_call.call_args_list[1][1]['to'],
self.contact2.phone)
# Check texts
self.assertEqual(mock_text.call_count, 1)
self.assertEqual(mock_text.call_args[1]['to'],
self.contact2.phone)
# Check URLs
urlparams = {
'sid': convert_superevent_id_to_speech(
self.superevent.superevent_id),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['superevent']['update'],
params=urlencode(urlparams))
# Check URL
for call_args in mock_call.call_args_list:
self.assertEqual(call_args[1]['url'], expected_url)
def test_label_added_superevent(self, mock_call, mock_text):
"""Test issuing phone alerts for label_added superevent alert"""
issue_phone_alerts(self.superevent, 'label_added', self.contacts,
label=self.label)
# Check calls
self.assertEqual(mock_call.call_count, 2)
self.assertEqual(mock_call.call_args_list[0][1]['to'],
self.contact1.phone)
self.assertEqual(mock_call.call_args_list[1][1]['to'],
self.contact2.phone)
# Check texts
self.assertEqual(mock_text.call_count, 1)
self.assertEqual(mock_text.call_args[1]['to'],
self.contact2.phone)
# Check URLs
urlparams = {
'sid': convert_superevent_id_to_speech(
self.superevent.superevent_id),
'label_lower': self.label.name.lower(),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['superevent']['label_added'],
params=urlencode(urlparams))
# Check URL
for call_args in mock_call.call_args_list:
self.assertEqual(call_args[1]['url'], expected_url)
def test_label_removed_superevent(self, mock_call, mock_text):
"""Test issuing phone alerts for label_removed superevent alert"""
issue_phone_alerts(self.superevent, 'label_removed', self.contacts,
label=self.label)
# Check calls
self.assertEqual(mock_call.call_count, 2)
self.assertEqual(mock_call.call_args_list[0][1]['to'],
self.contact1.phone)
self.assertEqual(mock_call.call_args_list[1][1]['to'],
self.contact2.phone)
# Check texts
self.assertEqual(mock_text.call_count, 1)
self.assertEqual(mock_text.call_args[1]['to'],
self.contact2.phone)
# Check URLs
urlparams = {
'sid': convert_superevent_id_to_speech(
self.superevent.superevent_id),
'label_lower': self.label.name.lower(),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['superevent']['label_removed'],
params=urlencode(urlparams))
# Check URL
for call_args in mock_call.call_args_list:
self.assertEqual(call_args[1]['url'], expected_url)
def test_new_event(self, mock_call, mock_text):
"""Test issuing phone alerts for new event alert"""
issue_phone_alerts(self.event, 'new', self.contacts)
# Check calls
self.assertEqual(mock_call.call_count, 2)
self.assertEqual(mock_call.call_args_list[0][1]['to'],
self.contact1.phone)
self.assertEqual(mock_call.call_args_list[1][1]['to'],
self.contact2.phone)
# Check texts
self.assertEqual(mock_text.call_count, 1)
self.assertEqual(mock_text.call_args[1]['to'],
self.contact2.phone)
# Check URLs
urlparams = {
'graceid': self.event.graceid,
'pipeline': self.event.pipeline.name,
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['event']['new'],
params=urlencode(urlparams))
# Check URL
for call_args in mock_call.call_args_list:
self.assertEqual(call_args[1]['url'], expected_url)
def test_update_event(self, mock_call, mock_text):
"""Test issuing phone alerts for event update alert"""
issue_phone_alerts(self.event, 'update', self.contacts)
# Check calls
self.assertEqual(mock_call.call_count, 2)
self.assertEqual(mock_call.call_args_list[0][1]['to'],
self.contact1.phone)
self.assertEqual(mock_call.call_args_list[1][1]['to'],
self.contact2.phone)
# Check texts
self.assertEqual(mock_text.call_count, 1)
self.assertEqual(mock_text.call_args[1]['to'],
self.contact2.phone)
# Check URLs
urlparams = {
'graceid': self.event.graceid,
'pipeline': self.event.pipeline.name,
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['event']['update'],
params=urlencode(urlparams))
# Check URL
for call_args in mock_call.call_args_list:
self.assertEqual(call_args[1]['url'], expected_url)
def test_label_added_event(self, mock_call, mock_text):
"""Test issuing phone alerts for label_added event alert"""
issue_phone_alerts(self.event, 'label_added', self.contacts,
self.label)
# Check calls
self.assertEqual(mock_call.call_count, 2)
self.assertEqual(mock_call.call_args_list[0][1]['to'],
self.contact1.phone)
self.assertEqual(mock_call.call_args_list[1][1]['to'],
self.contact2.phone)
# Check texts
self.assertEqual(mock_text.call_count, 1)
self.assertEqual(mock_text.call_args[1]['to'],
self.contact2.phone)
# Check URLs
urlparams = {
'graceid': self.event.graceid,
'pipeline': self.event.pipeline.name,
'label_lower': self.label.name.lower(),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['event']['label_added'],
params=urlencode(urlparams))
# Check URL
for call_args in mock_call.call_args_list:
self.assertEqual(call_args[1]['url'], expected_url)
def test_label_removed_event(self, mock_call, mock_text):
"""Test issuing phone alerts for label_removed event alert"""
issue_phone_alerts(self.event, 'label_removed', self.contacts,
self.label)
# Check calls
self.assertEqual(mock_call.call_count, 2)
self.assertEqual(mock_call.call_args_list[0][1]['to'],
self.contact1.phone)
self.assertEqual(mock_call.call_args_list[1][1]['to'],
self.contact2.phone)
# Check texts
self.assertEqual(mock_text.call_count, 1)
self.assertEqual(mock_text.call_args[1]['to'],
self.contact2.phone)
# Check URLs
urlparams = {
'graceid': self.event.graceid,
'pipeline': self.event.pipeline.name,
'label_lower': self.label.name.lower(),
}
expected_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['event']['label_removed'],
params=urlencode(urlparams))
# Check URL
for call_args in mock_call.call_args_list:
self.assertEqual(call_args[1]['url'], expected_url)
class TestContactCleanErrors(GraceDbTestBase):
def helper_test_contact_produces_error(self, err_field, err_message,
**contact_kwargs):
error_raised = False
try:
# Create a Contact that should trigger a ValidationError
Contact.objects.create(user=self.internal_user,
description='malformed contact', verified=True,
**contact_kwargs)
except ValidationError as err:
error_raised = True
# Check that the error messages have the correct entry.
error_messages = err.message_dict
self.assertIn(err_field, error_messages)
self.assertIn(err_message, error_messages[err_field])
# Should fail if no error raised
self.assertTrue(error_raised)
def helper_test_missing_phone_raises_error(self, phone_method):
"""Test checking that phone number required if phone method set"""
self.helper_test_contact_produces_error(
'phone', '"Call" and "text" should be False for non-phone alerts.',
phone_method=phone_method,
)
def test_missing_phone_raises_error_method_call(self):
self.helper_test_missing_phone_raises_error(Contact.CONTACT_PHONE_CALL)
def test_missing_phone_raises_error_method_text(self):
self.helper_test_missing_phone_raises_error(Contact.CONTACT_PHONE_TEXT)
def test_missing_phone_raises_error_method_both(self):
self.helper_test_missing_phone_raises_error(Contact.CONTACT_PHONE_BOTH)
def test_phone_without_method_raises_error(self):
"""Test checking that phone_method is required if phone set"""
self.helper_test_contact_produces_error(
'phone_method', 'Choose a phone contact method.',
phone='12345678901')
def test_phone_email_mutually_exclusive(self):
"""Test checking phone and email mutually exclusive"""
self.helper_test_contact_produces_error(
NON_FIELD_ERRORS,
'Only one contact method (email or phone) can be selected.',
phone='12345678901', email='albert.einstein@ligo.org')
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
import types
from alerts.models import Notification
from alerts.recipients import (
CreationRecipientGetter, UpdateRecipientGetter,
LabelAddedRecipientGetter, LabelRemovedRecipientGetter,
)
from events.models import Label, Group, Pipeline, Search
from .constants import (
DEFAULT_FAR_T, DEFAULT_LABELS, DEFAULT_LABEL_QUERY, LABEL_QUERY2,
RANDOM_LABEL, DEFAULT_GROUP, DEFAULT_PIPELINE, DEFAULT_SEARCH
)
# NOTE: there are a *LOT* of tests in here. It's definitely overkill.
# I'm trying to test *every* possible situation because users tend
# to get worked up over problems with notifications.
#
# NOTE on debugging: because there are only a few actual test functions, but
# they are highly parametrized, it can be hard to debug failing tests. I
# suggest looking at the test label in the pytest output (i.e., you should
# see something like "notif_descs###", where ### is the test number). Then
# go and edit the dataset used in the parametrize decorator to be just that
# test. Ex: if test_event_update_alerts with notif_descs123 is failing,
# go and edit EVENT_UPDATE_ALERT_DATA -> EVENT_UPDATE_ALERT_DATA[123:124]
###############################################################################
# TEST DATA ###################################################################
###############################################################################
SUPEREVENT_CREATION_ALERT_DATA = [
(None, False, ['all']),
(None, True, ['all', 'nscand_only']),
(DEFAULT_FAR_T*2.0, False, ['all']),
(DEFAULT_FAR_T*2.0, True, ['all', 'nscand_only']),
(DEFAULT_FAR_T/2.0, False, ['all', 'far_t_only']),
(DEFAULT_FAR_T/2.0, True, ['all', 'far_t_only', 'nscand_only',
'far_t_and_nscand']),
]
EVENT_CREATION_ALERT_DATA = [
(None, False, False, ['all']),
(None, False, True, ['all', 'gps_only']),
(None, True, False, ['all', 'nscand_only']),
(None, True, True, ['all', 'nscand_only', 'gps_only', 'nscand_and_gps']),
(DEFAULT_FAR_T*2.0, False, False, ['all']),
(DEFAULT_FAR_T*2.0, False, True, ['all', 'gps_only']),
(DEFAULT_FAR_T*2.0, True, False, ['all', 'nscand_only']),
(DEFAULT_FAR_T*2.0, True, True, ['all', 'nscand_only', 'gps_only',
'nscand_and_gps']),
(DEFAULT_FAR_T/2.0, False, False, ['all', 'far_t_only']),
(DEFAULT_FAR_T/2.0, False, True, ['all', 'far_t_only', 'gps_only',
'far_t_and_gps']),
(DEFAULT_FAR_T/2.0, True, False, ['all', 'far_t_only', 'nscand_only',
'far_t_and_nscand']),
(DEFAULT_FAR_T/2.0, True, True,
['all', 'far_t_only', 'nscand_only', 'gps_only', 'far_t_and_nscand',
'far_t_and_gps', 'nscand_and_gps', 'far_t_and_nscand_and_gps']),
]
SUPEREVENT_UPDATE_ALERT_DATA = [
# FAR = None and constant -------------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(None, None, False, False, None, []),
(None, None, True, True, None, []),
### With labels -----------------------------------
(None, None, False, False, DEFAULT_LABELS, []),
(None, None, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(None, None, False, False, DEFAULT_LABEL_QUERY['labels'], []),
(None, None, True, True, DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(None, None, False, True, None, ['nscand_only']),
(None, None, True, False, None, []),
### With labels -----------------------------------
(None, None, False, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(None, None, True, False, DEFAULT_LABELS, []),
### With labels matching label query --------------
(None, None, False, True, DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq']),
(None, None, True, False, DEFAULT_LABEL_QUERY['labels'], []),
# FAR = above threshold and constant --------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, None, ['nscand_only']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True,
DEFAULT_LABEL_QUERY['labels'], ['nscand_only',
'nscand_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = above threshold and increases -------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, None, ['nscand_only']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True,
DEFAULT_LABEL_QUERY['labels'], ['nscand_only',
'nscand_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = below threshold and constant --------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, None,
['nscand_only', 'far_t_and_nscand']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, DEFAULT_LABELS,
['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True,
DEFAULT_LABEL_QUERY['labels'], ['nscand_only', 'far_t_and_nscand',
'nscand_and_labelq',
'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = below threshold and decreases -------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, None,
['nscand_only', 'far_t_and_nscand']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, DEFAULT_LABELS,
['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True,
DEFAULT_LABEL_QUERY['labels'], ['nscand_only', 'far_t_and_nscand',
'nscand_and_labelq',
'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = below -> above threshold ------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, None,
['nscand_only']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = None -> above threshold -------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(None, DEFAULT_FAR_T*2, False, False, None, []),
(None, DEFAULT_FAR_T*2, True, True, None, []),
### With labels -----------------------------------
(None, DEFAULT_FAR_T*2, False, False, DEFAULT_LABELS, []),
(None, DEFAULT_FAR_T*2, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(None, DEFAULT_FAR_T*2, False, False, DEFAULT_LABEL_QUERY['labels'], []),
(None, DEFAULT_FAR_T*2, True, True, DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(None, DEFAULT_FAR_T*2, False, True, None, ['nscand_only']),
(None, DEFAULT_FAR_T*2, True, False, None, []),
### With labels -----------------------------------
(None, DEFAULT_FAR_T*2, False, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(None, DEFAULT_FAR_T*2, True, False, DEFAULT_LABELS, []),
### With labels matching label query --------------
(None, DEFAULT_FAR_T*2, False, True, DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq']),
(None, DEFAULT_FAR_T*2, True, False, DEFAULT_LABEL_QUERY['labels'], []),
# FAR = None -> below threshold -------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(None, DEFAULT_FAR_T/2.0, False, False, None, ['far_t_only']),
(None, DEFAULT_FAR_T/2.0, True, True, None,
['far_t_only', 'far_t_and_nscand']),
### With labels -----------------------------------
(None, DEFAULT_FAR_T/2.0, False, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels']),
(None, DEFAULT_FAR_T/2.0, True, True, DEFAULT_LABELS,
['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
'far_t_and_nscand_and_labels']),
### With labels matching label query --------------
(None, DEFAULT_FAR_T/2.0, False, False, DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq']),
(None, DEFAULT_FAR_T/2.0, True, True, DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
'far_t_and_nscand_and_labelq']),
# FAR = None -> below threshold -------------------
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(None, DEFAULT_FAR_T/2.0, False, True, None,
['far_t_only', 'nscand_only', 'far_t_and_nscand']),
(None, DEFAULT_FAR_T/2.0, True, False, None, ['far_t_only']),
### With labels -----------------------------------
(None, DEFAULT_FAR_T/2.0, False, True, DEFAULT_LABELS,
['far_t_only', 'nscand_only', 'far_t_and_nscand',
'far_t_and_labels', 'nscand_and_labels',
'far_t_and_nscand_and_labels']),
(None, DEFAULT_FAR_T/2.0, True, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels']),
### With labels matching label query --------------
(None, DEFAULT_FAR_T/2.0, False, True, DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'nscand_only', 'far_t_and_nscand',
'far_t_and_labelq', 'nscand_and_labelq',
'far_t_and_nscand_and_labelq']),
(None, DEFAULT_FAR_T/2.0, True, False, DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq']),
# FAR = above -> below threshold ------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, None,
['far_t_only']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, None,
['far_t_only', 'far_t_and_nscand']),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, DEFAULT_LABELS,
['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
'far_t_and_nscand_and_labels']),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
'far_t_and_nscand_and_labelq']),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, None,
['far_t_only', 'nscand_only', 'far_t_and_nscand']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, None,
['far_t_only']),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, DEFAULT_LABELS,
['far_t_only', 'nscand_only', 'far_t_and_nscand',
'far_t_and_labels', 'nscand_and_labels',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels']),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'nscand_only', 'far_t_and_nscand',
'far_t_and_labelq', 'nscand_and_labelq',
'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq']),
]
# Params: old_far,far,old_nscand,nscand,use_default_gps,labels,notif_descs
EVENT_UPDATE_ALERT_DATA = [
# FAR = None and constant -------------------------
## No labels --------------------------------------
### NSCAND = constant -----------------------------
(None, None, False, False, False, None, []),
(None, None, False, False, True, None, []),
(None, None, True, True, False, None, []),
(None, None, True, True, True, None, []),
### With labels -----------------------------------
(None, None, False, False, False, DEFAULT_LABELS, []),
(None, None, False, False, True, DEFAULT_LABELS, []),
(None, None, True, True, False, DEFAULT_LABELS, []),
(None, None, True, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(None, None, False, False, False, DEFAULT_LABEL_QUERY['labels'], []),
(None, None, False, False, True, DEFAULT_LABEL_QUERY['labels'], []),
(None, None, True, True, False, DEFAULT_LABEL_QUERY['labels'], []),
(None, None, True, True, True, DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(None, None, False, True, False, None, ['nscand_only']),
(None, None, False, True, True, None, ['nscand_only', 'nscand_and_gps']),
(None, None, True, False, False, None, []),
(None, None, True, False, True, None, []),
### With labels -----------------------------------
(None, None, False, True, False, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(None, None, False, True, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
'nscand_and_labels_and_gps']),
(None, None, True, False, False, DEFAULT_LABELS, []),
(None, None, True, False, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(None, None, False, True, False, DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq']),
(None, None, False, True, True, DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
'nscand_and_labelq_and_gps']),
(None, None, True, False, False, DEFAULT_LABEL_QUERY['labels'], []),
(None, None, True, False, True, DEFAULT_LABEL_QUERY['labels'], []),
# FAR = above threshold and constant --------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, False, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, True, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, False, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, True, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, False, None,
['nscand_only']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, True, None,
['nscand_only', 'nscand_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, False, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, False, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
'nscand_and_labels_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, False,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, False, True, True,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
'nscand_and_labelq_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*2, True, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = above threshold and increases -------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, False, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, True, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, False, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, True, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, False, None,
['nscand_only']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, True, None,
['nscand_only', 'nscand_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, False, None, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, False, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
'nscand_and_labels_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, False, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, False,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, False, True, True,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
'nscand_and_labelq_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T*4, True, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = below threshold and constant --------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, True, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, True, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, True, DEFAULT_LABELS,
[]),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, False, None,
['nscand_only', 'far_t_and_nscand']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, True, None,
['nscand_only', 'far_t_and_nscand', 'nscand_and_gps',
'far_t_and_nscand_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, False, DEFAULT_LABELS,
['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, True, DEFAULT_LABELS,
['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
'far_t_and_nscand_and_labels', 'nscand_and_gps',
'far_t_and_nscand_and_gps', 'nscand_and_labels_and_gps',
'far_t_and_nscand_and_labels_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, True, DEFAULT_LABELS,
[]),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, False,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'far_t_and_nscand', 'nscand_and_labelq',
'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, False, True, True,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'far_t_and_nscand', 'nscand_and_labelq',
'far_t_and_nscand_and_labelq', 'nscand_and_gps',
'far_t_and_nscand_and_gps', 'nscand_and_labelq_and_gps',
'far_t_and_nscand_and_labelq_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/2.0, True, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = below threshold and decreases -------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, True, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, True, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, True, DEFAULT_LABELS,
[]),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, False, None,
['nscand_only', 'far_t_and_nscand']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, True, None,
['nscand_only', 'far_t_and_nscand', 'nscand_and_gps',
'far_t_and_nscand_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, False, DEFAULT_LABELS,
['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, True, DEFAULT_LABELS,
['nscand_only', 'far_t_and_nscand', 'nscand_and_labels',
'far_t_and_nscand_and_labels', 'nscand_and_gps',
'far_t_and_nscand_and_gps', 'nscand_and_labels_and_gps',
'far_t_and_nscand_and_labels_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, True, DEFAULT_LABELS,
[]),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, False,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'far_t_and_nscand', 'nscand_and_labelq',
'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, False, True, True,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'far_t_and_nscand', 'nscand_and_labelq',
'far_t_and_nscand_and_labelq', 'nscand_and_gps',
'far_t_and_nscand_and_gps', 'nscand_and_labelq_and_gps',
'far_t_and_nscand_and_labelq_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T/4.0, True, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = below -> above threshold ------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, True, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, True, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, True, DEFAULT_LABELS,
[]),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, True, True,
DEFAULT_LABEL_QUERY['labels'], []),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, False, None,
['nscand_only']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, True, None,
['nscand_only', 'nscand_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, False, None, []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, True, None, []),
### With labels -----------------------------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, False, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
'nscand_and_labels_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, False, DEFAULT_LABELS,
[]),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, True, DEFAULT_LABELS,
[]),
### With labels matching label query --------------
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, False,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, False, True, True,
DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
'nscand_and_labelq_and_gps']),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, False,
DEFAULT_LABEL_QUERY['labels'], []),
(DEFAULT_FAR_T/2.0, DEFAULT_FAR_T*2, True, False, True,
DEFAULT_LABEL_QUERY['labels'], []),
# FAR = None -> above threshold -------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(None, DEFAULT_FAR_T*2, False, False, False, None, []),
(None, DEFAULT_FAR_T*2, False, False, True, None, []),
(None, DEFAULT_FAR_T*2, True, True, False, None, []),
(None, DEFAULT_FAR_T*2, True, True, True, None, []),
### With labels -----------------------------------
(None, DEFAULT_FAR_T*2, False, False, False, DEFAULT_LABELS, []),
(None, DEFAULT_FAR_T*2, False, False, True, DEFAULT_LABELS, []),
(None, DEFAULT_FAR_T*2, True, True, False, DEFAULT_LABELS, []),
(None, DEFAULT_FAR_T*2, True, True, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(None, DEFAULT_FAR_T*2, False, False, False, DEFAULT_LABEL_QUERY['labels'],
[]),
(None, DEFAULT_FAR_T*2, False, False, True, DEFAULT_LABEL_QUERY['labels'],
[]),
(None, DEFAULT_FAR_T*2, True, True, False, DEFAULT_LABEL_QUERY['labels'],
[]),
(None, DEFAULT_FAR_T*2, True, True, True, DEFAULT_LABEL_QUERY['labels'],
[]),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(None, DEFAULT_FAR_T*2, False, True, False, None, ['nscand_only']),
(None, DEFAULT_FAR_T*2, False, True, True, None,
['nscand_only', 'nscand_and_gps']),
(None, DEFAULT_FAR_T*2, True, False, False, None, []),
(None, DEFAULT_FAR_T*2, True, False, True, None, []),
### With labels -----------------------------------
(None, DEFAULT_FAR_T*2, False, True, False, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels']),
(None, DEFAULT_FAR_T*2, False, True, True, DEFAULT_LABELS,
['nscand_only', 'nscand_and_labels', 'nscand_and_gps',
'nscand_and_labels_and_gps']),
(None, DEFAULT_FAR_T*2, True, False, False, DEFAULT_LABELS, []),
(None, DEFAULT_FAR_T*2, True, False, True, DEFAULT_LABELS, []),
### With labels matching label query --------------
(None, DEFAULT_FAR_T*2, False, True, False, DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq']),
(None, DEFAULT_FAR_T*2, False, True, True, DEFAULT_LABEL_QUERY['labels'],
['nscand_only', 'nscand_and_labelq', 'nscand_and_gps',
'nscand_and_labelq_and_gps']),
(None, DEFAULT_FAR_T*2, True, False, False, DEFAULT_LABEL_QUERY['labels'],
[]),
(None, DEFAULT_FAR_T*2, True, False, True, DEFAULT_LABEL_QUERY['labels'],
[]),
# FAR = None -> below threshold -------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(None, DEFAULT_FAR_T/2.0, False, False, False, None, ['far_t_only']),
(None, DEFAULT_FAR_T/2.0, False, False, True, None,
['far_t_only', 'far_t_and_gps']),
(None, DEFAULT_FAR_T/2.0, True, True, False, None,
['far_t_only', 'far_t_and_nscand']),
(None, DEFAULT_FAR_T/2.0, True, True, True, None,
['far_t_only', 'far_t_and_nscand', 'far_t_and_gps',
'far_t_and_nscand_and_gps']),
### With labels -----------------------------------
(None, DEFAULT_FAR_T/2.0, False, False, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels']),
(None, DEFAULT_FAR_T/2.0, False, False, True, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels', 'far_t_and_gps',
'far_t_and_labels_and_gps']),
(None, DEFAULT_FAR_T/2.0, True, True, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
'far_t_and_nscand_and_labels']),
(None, DEFAULT_FAR_T/2.0, True, True, True, DEFAULT_LABELS,
['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
'far_t_and_nscand_and_labels', 'far_t_and_gps',
'far_t_and_nscand_and_gps', 'far_t_and_labels_and_gps',
'far_t_and_nscand_and_labels_and_gps']),
### With labels matching label query --------------
(None, DEFAULT_FAR_T/2.0, False, False, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq']),
(None, DEFAULT_FAR_T/2.0, False, False, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq', 'far_t_and_gps',
'far_t_and_labelq_and_gps']),
(None, DEFAULT_FAR_T/2.0, True, True, False, DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
'far_t_and_nscand_and_labelq']),
(None, DEFAULT_FAR_T/2.0, True, True, True, DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
'far_t_and_nscand_and_labelq', 'far_t_and_gps',
'far_t_and_nscand_and_gps', 'far_t_and_labelq_and_gps',
'far_t_and_nscand_and_labelq_and_gps']),
# FAR = None -> below threshold -------------------
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(None, DEFAULT_FAR_T/2.0, False, True, False, None,
['far_t_only', 'nscand_only', 'far_t_and_nscand']),
(None, DEFAULT_FAR_T/2.0, False, True, True, None,
['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_gps',
'nscand_and_gps', 'far_t_and_nscand_and_gps']),
(None, DEFAULT_FAR_T/2.0, True, False, False, None, ['far_t_only']),
(None, DEFAULT_FAR_T/2.0, True, False, True, None,
['far_t_only', 'far_t_and_gps']),
### With labels -----------------------------------
(None, DEFAULT_FAR_T/2.0, False, True, False, DEFAULT_LABELS,
['far_t_only', 'nscand_only', 'far_t_and_nscand',
'far_t_and_labels', 'nscand_and_labels',
'far_t_and_nscand_and_labels']),
(None, DEFAULT_FAR_T/2.0, False, True, True, DEFAULT_LABELS,
['far_t_only', 'nscand_only', 'far_t_and_nscand',
'far_t_and_labels', 'nscand_and_labels',
'far_t_and_nscand_and_labels', 'far_t_and_gps', 'nscand_and_gps',
'far_t_and_nscand_and_gps', 'far_t_and_labels_and_gps',
'nscand_and_labels_and_gps', 'far_t_and_nscand_and_labels_and_gps']),
(None, DEFAULT_FAR_T/2.0, True, False, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels']),
(None, DEFAULT_FAR_T/2.0, True, False, True, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels', 'far_t_and_gps',
'far_t_and_labels_and_gps']),
### With labels matching label query --------------
(None, DEFAULT_FAR_T/2.0, False, True, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labelq',
'nscand_and_labelq', 'far_t_and_nscand_and_labelq']),
(None, DEFAULT_FAR_T/2.0, False, True, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labelq',
'nscand_and_labelq', 'far_t_and_nscand_and_labelq',
'far_t_and_gps', 'nscand_and_gps', 'far_t_and_nscand_and_gps',
'far_t_and_labelq_and_gps', 'nscand_and_labelq_and_gps',
'far_t_and_nscand_and_labelq_and_gps']),
(None, DEFAULT_FAR_T/2.0, True, False, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq']),
(None, DEFAULT_FAR_T/2.0, True, False, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq', 'far_t_and_gps',
'far_t_and_labelq_and_gps']),
# FAR = above -> below threshold ------------------
## NSCAND = constant ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, False, None,
['far_t_only']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, True, None,
['far_t_only', 'far_t_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, False, None,
['far_t_only', 'far_t_and_nscand']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, True, None,
['far_t_only', 'far_t_and_nscand', 'far_t_and_gps',
'far_t_and_nscand_and_gps']),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, True, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels', 'far_t_and_gps',
'far_t_and_labels_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, True, DEFAULT_LABELS,
['far_t_only', 'far_t_and_nscand', 'far_t_and_labels',
'far_t_and_nscand_and_labels', 'far_t_and_gps',
'far_t_and_nscand_and_gps', 'far_t_and_labels_and_gps',
'far_t_and_nscand_and_labels_and_gps']),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, False, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq', 'far_t_and_gps',
'far_t_and_labelq_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, True, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_nscand', 'far_t_and_labelq',
'far_t_and_nscand_and_labelq', 'far_t_and_gps',
'far_t_and_nscand_and_gps', 'far_t_and_labelq_and_gps',
'far_t_and_nscand_and_labelq_and_gps']),
## NSCAND = changing ------------------------------
### No labels -------------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, False, None,
['far_t_only', 'nscand_only', 'far_t_and_nscand']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, True, None,
['far_t_only', 'nscand_only', 'far_t_and_nscand',
'far_t_and_gps', 'nscand_and_gps', 'far_t_and_nscand_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, False, None,
['far_t_only']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, True, None,
['far_t_only', 'far_t_and_gps']),
### With labels -----------------------------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, False, DEFAULT_LABELS,
['far_t_only', 'nscand_only', 'far_t_and_nscand',
'far_t_and_labels', 'nscand_and_labels',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, True, DEFAULT_LABELS,
['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labels',
'nscand_and_labels', 'far_t_and_nscand_and_labels', 'far_t_and_gps',
'nscand_and_gps', 'far_t_and_nscand_and_gps',
'far_t_and_labels_and_gps', 'nscand_and_labels_and_gps',
'far_t_and_nscand_and_labels_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, False, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, True, DEFAULT_LABELS,
['far_t_only', 'far_t_and_labels', 'far_t_and_gps',
'far_t_and_labels_and_gps']),
### With labels matching label query --------------
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labelq',
'nscand_and_labelq', 'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, False, True, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'nscand_only', 'far_t_and_nscand', 'far_t_and_labelq',
'nscand_and_labelq', 'far_t_and_nscand_and_labelq', 'far_t_and_gps',
'nscand_and_gps', 'far_t_and_nscand_and_gps',
'far_t_and_labelq_and_gps', 'nscand_and_labelq_and_gps',
'far_t_and_nscand_and_labelq_and_gps']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, False,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq']),
(DEFAULT_FAR_T*2, DEFAULT_FAR_T/2.0, True, False, True,
DEFAULT_LABEL_QUERY['labels'],
['far_t_only', 'far_t_and_labelq', 'far_t_and_gps',
'far_t_and_labelq_and_gps']),
]
SUPEREVENT_LABEL_ADDED_ALERT_DATA = [
# Label criteria not met --------------------------
## FAR = None -------------------------------------
(None, False, DEFAULT_LABELS[0], None, []),
(None, True, DEFAULT_LABELS[0], None, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], None, []),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], None, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], None, []),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], None, []),
# Label criteria met ------------------------------
## FAR = None -------------------------------------
(None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
['labels_only']),
(None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
['nscand_and_labels', 'labels_only']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
['labels_only']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
['nscand_and_labels', 'labels_only']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
['far_t_and_labels', 'labels_only']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:],
['far_t_and_labels', 'nscand_and_labels', 'labels_only',
'far_t_and_nscand_and_labels']),
# Label criteria met, some additional labels previously added
## FAR = None -------------------------------------
(None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
['labels_only']),
(None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
['nscand_and_labels', 'labels_only']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], ['labels_only']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL],
['nscand_and_labels', 'labels_only']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL],
['far_t_and_labels', 'labels_only']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL],
['far_t_and_labels', 'nscand_and_labels', 'labels_only',
'far_t_and_nscand_and_labels']),
# Label criteria previously met -------------------
## FAR = None -------------------------------------
(None, False, RANDOM_LABEL, DEFAULT_LABELS, []),
(None, True, RANDOM_LABEL, DEFAULT_LABELS, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABELS, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABELS, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABELS, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABELS, []),
# Label query criteria not met --------------------
## FAR = None -------------------------------------
(None, False, DEFAULT_LABEL_QUERY['labels'][0], None, []),
(None, True, DEFAULT_LABEL_QUERY['labels'][0], None, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, []),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, []),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, []),
# Label query criteria met -------------------------
## FAR = None -------------------------------------
(None, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], ['labelq_only']),
(None, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:],
['nscand_and_labelq', 'labelq_only']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], ['labelq_only']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:],
['nscand_and_labelq', 'labelq_only']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:],
['far_t_and_labelq', 'labelq_only']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:],
['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
'far_t_and_nscand_and_labelq']),
# Label query criteria met, some additional labels previously added
## FAR = None -------------------------------------
(None, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
['labelq_only']),
(None, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
['nscand_and_labelq', 'labelq_only']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
['labelq_only']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
['nscand_and_labelq', 'labelq_only']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
['far_t_and_labelq', 'labelq_only']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL],
['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
'far_t_and_nscand_and_labelq']),
# Label query criteria previously met -------------
## FAR = None -------------------------------------
(None, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], []),
(None, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
[]),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
[]),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], []),
]
# Params: far,nscand,new_label,old_labels,use_default_gps,notif_descs
EVENT_LABEL_ADDED_ALERT_DATA = [
# Label criteria not met --------------------------
## FAR = None -------------------------------------
(None, False, DEFAULT_LABELS[0], None, False, []),
(None, False, DEFAULT_LABELS[0], None, True, []),
(None, True, DEFAULT_LABELS[0], None, False, []),
(None, True, DEFAULT_LABELS[0], None, True, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], None, False, []),
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], None, True, []),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], None, False, []),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], None, True, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], None, False, []),
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], None, True, []),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], None, False, []),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], None, True, []),
# Label criteria met ------------------------------
## FAR = None -------------------------------------
(None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
['labels_only']),
(None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
['labels_only', 'labels_and_gps']),
(None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
['nscand_and_labels', 'labels_only']),
(None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
['nscand_and_labels', 'labels_only', 'nscand_and_labels_and_gps',
'labels_and_gps']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
['labels_only']),
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
['labels_only', 'labels_and_gps']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
['nscand_and_labels', 'labels_only']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
['nscand_and_labels', 'labels_only', 'nscand_and_labels_and_gps',
'labels_and_gps']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
['far_t_and_labels', 'labels_only']),
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
['far_t_and_labels', 'labels_only', 'far_t_and_labels_and_gps',
'labels_and_gps']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], False,
['far_t_and_labels', 'nscand_and_labels', 'labels_only',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:], True,
['far_t_and_labels', 'nscand_and_labels', 'labels_only',
'far_t_and_nscand_and_labels', 'far_t_and_labels_and_gps',
'nscand_and_labels_and_gps', 'labels_and_gps',
'far_t_and_nscand_and_labels_and_gps']),
# Label criteria met, some additional labels previously added
## FAR = None -------------------------------------
(None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
False, ['labels_only']),
(None, False, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
True, ['labels_only', 'labels_and_gps']),
(None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
False, ['nscand_and_labels', 'labels_only']),
(None, True, DEFAULT_LABELS[0], DEFAULT_LABELS[1:] + [RANDOM_LABEL],
True, ['nscand_and_labels', 'labels_only', 'labels_and_gps',
'nscand_and_labels_and_gps']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], False, ['labels_only']),
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], True,
['labels_only', 'labels_and_gps']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], False,
['nscand_and_labels', 'labels_only']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], True,
['nscand_and_labels', 'labels_only', 'labels_and_gps',
'nscand_and_labels_and_gps']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], False,
['far_t_and_labels', 'labels_only']),
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], True,
['far_t_and_labels', 'labels_only', 'labels_and_gps',
'far_t_and_labels_and_gps']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], False,
['far_t_and_labels', 'nscand_and_labels', 'labels_only',
'far_t_and_nscand_and_labels']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABELS[0],
DEFAULT_LABELS[1:] + [RANDOM_LABEL], True,
['far_t_and_labels', 'nscand_and_labels', 'labels_only',
'far_t_and_nscand_and_labels', 'far_t_and_labels_and_gps',
'nscand_and_labels_and_gps', 'labels_and_gps',
'far_t_and_nscand_and_labels_and_gps']),
# Label criteria previously met -------------------
## FAR = None -------------------------------------
(None, False, RANDOM_LABEL, DEFAULT_LABELS, False, []),
(None, False, RANDOM_LABEL, DEFAULT_LABELS, True, []),
(None, True, RANDOM_LABEL, DEFAULT_LABELS, False, []),
(None, True, RANDOM_LABEL, DEFAULT_LABELS, True, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABELS, False, []),
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABELS, True, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABELS, False, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABELS, True, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABELS, False, []),
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABELS, True, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABELS, False, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABELS, True, []),
# Label query criteria not met --------------------
## FAR = None -------------------------------------
(None, False, DEFAULT_LABEL_QUERY['labels'][0], None, False, []),
(None, False, DEFAULT_LABEL_QUERY['labels'][0], None, True, []),
(None, True, DEFAULT_LABEL_QUERY['labels'][0], None, False, []),
(None, True, DEFAULT_LABEL_QUERY['labels'][0], None, True, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, False,
[]),
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, True,
[]),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, False,
[]),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, True,
[]),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, False,
[]),
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0], None, True,
[]),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, False,
[]),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0], None, True,
[]),
# Label query criteria met -------------------------
## FAR = None -------------------------------------
(None, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], False, ['labelq_only']),
(None, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], True,
['labelq_only', 'labelq_and_gps']),
(None, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], False,
['nscand_and_labelq', 'labelq_only']),
(None, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], True,
['nscand_and_labelq', 'labelq_only', 'labelq_and_gps',
'nscand_and_labelq_and_gps']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], False, ['labelq_only']),
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], True,
['labelq_only', 'labelq_and_gps']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], False,
['nscand_and_labelq', 'labelq_only']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], True,
['nscand_and_labelq', 'labelq_only', 'labelq_and_gps',
'nscand_and_labelq_and_gps']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], False,
['far_t_and_labelq', 'labelq_only']),
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], True,
['far_t_and_labelq', 'labelq_only', 'labelq_and_gps',
'far_t_and_labelq_and_gps']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], False,
['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:], True,
['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
'far_t_and_nscand_and_labelq', 'far_t_and_labelq_and_gps',
'nscand_and_labelq_and_gps', 'labelq_and_gps',
'far_t_and_nscand_and_labelq_and_gps']),
# Label query criteria met, some additional labels previously added
## FAR = None -------------------------------------
(None, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
['labelq_only']),
(None, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
['labelq_only', 'labelq_and_gps']),
(None, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
['nscand_and_labelq', 'labelq_only']),
(None, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
['nscand_and_labelq', 'labelq_only', 'labelq_and_gps',
'nscand_and_labelq_and_gps']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
['labelq_only']),
(DEFAULT_FAR_T*2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
['labelq_only', 'labelq_and_gps']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
['nscand_and_labelq', 'labelq_only']),
(DEFAULT_FAR_T*2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
['nscand_and_labelq', 'labelq_only', 'labelq_and_gps',
'nscand_and_labelq_and_gps']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
['far_t_and_labelq', 'labelq_only']),
(DEFAULT_FAR_T/2.0, False, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
['far_t_and_labelq', 'labelq_only', 'labelq_and_gps',
'far_t_and_labelq_and_gps']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], False,
['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
'far_t_and_nscand_and_labelq']),
(DEFAULT_FAR_T/2.0, True, DEFAULT_LABEL_QUERY['labels'][0],
DEFAULT_LABEL_QUERY['labels'][1:] + [RANDOM_LABEL], True,
['far_t_and_labelq', 'nscand_and_labelq', 'labelq_only',
'far_t_and_nscand_and_labelq', 'far_t_and_labelq_and_gps',
'nscand_and_labelq_and_gps', 'labelq_and_gps',
'far_t_and_nscand_and_labelq_and_gps']),
# Label query criteria previously met -------------
## FAR = None -------------------------------------
(None, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], False, []),
(None, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], True, []),
(None, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], False, []),
(None, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'], True, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
False, []),
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
True, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
False, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
True, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
False, []),
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
True, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
False, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, DEFAULT_LABEL_QUERY['labels'],
True, []),
]
SUPEREVENT_LABEL_REMOVED_ALERT_DATA = [
# Label criteria not met --------------------------
## FAR = None -------------------------------------
(None, False, RANDOM_LABEL, None, []),
(None, True, RANDOM_LABEL, None, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, None, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, None, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, None, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, None, []),
# Label criteria met ------------------------------
## FAR = None -------------------------------------
(None, False, 'L7', ['L5', 'L6'], ['labelq2_only']),
(None, True, 'L7', ['L5', 'L6'],
['nscand_and_labelq2', 'labelq2_only']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6'],
['labelq2_only']),
(DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6'],
['nscand_and_labelq2', 'labelq2_only']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6'],
['far_t_and_labelq2', 'labelq2_only']),
(DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6'],
['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
'far_t_and_nscand_and_labelq2']),
# Label criteria met, some additional labels previously added
## FAR = None -------------------------------------
(None, False, 'L7', ['L5', 'L6', RANDOM_LABEL],
['labelq2_only']),
(None, True, 'L7', ['L5', 'L6', RANDOM_LABEL],
['nscand_and_labelq2', 'labelq2_only']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL],
['labelq2_only']),
(DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL],
['nscand_and_labelq2', 'labelq2_only']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL],
['far_t_and_labelq2', 'labelq2_only']),
(DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL],
['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
'far_t_and_nscand_and_labelq2']),
# Label criteria previously met -------------------
## FAR = None -------------------------------------
(None, False, RANDOM_LABEL, ['L5', 'L6'], []),
(None, True, RANDOM_LABEL, ['L5', 'L6'], []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, ['L5', 'L6'], []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, ['L5', 'L6'], []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, ['L5', 'L6'], []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, ['L5', 'L6'], []),
]
# Params: far,nscand,removed_label,labels,use_default_gps,notif_descs
EVENT_LABEL_REMOVED_ALERT_DATA = [
# Label criteria not met --------------------------
## FAR = None -------------------------------------
(None, False, RANDOM_LABEL, None, False, []),
(None, False, RANDOM_LABEL, None, True, []),
(None, True, RANDOM_LABEL, None, False, []),
(None, True, RANDOM_LABEL, None, True, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, None, False, []),
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, None, True, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, None, False, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, None, True, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, None, False, []),
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, None, True, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, None, False, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, None, True, []),
# Label criteria met ------------------------------
## FAR = None -------------------------------------
(None, False, 'L7', ['L5', 'L6'], False, ['labelq2_only']),
(None, False, 'L7', ['L5', 'L6'], True,
['labelq2_only', 'labelq2_and_gps']),
(None, True, 'L7', ['L5', 'L6'], False,
['nscand_and_labelq2', 'labelq2_only']),
(None, True, 'L7', ['L5', 'L6'], True,
['nscand_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
'nscand_and_labelq2_and_gps']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6'], False,
['labelq2_only']),
(DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6'], True,
['labelq2_only', 'labelq2_and_gps']),
(DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6'], False,
['nscand_and_labelq2', 'labelq2_only']),
(DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6'], True,
['nscand_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
'nscand_and_labelq2_and_gps']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6'], False,
['far_t_and_labelq2', 'labelq2_only']),
(DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6'], True,
['far_t_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
'far_t_and_labelq2_and_gps']),
(DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6'], False,
['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
'far_t_and_nscand_and_labelq2']),
(DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6'], True,
['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
'far_t_and_nscand_and_labelq2', 'labelq2_and_gps',
'far_t_and_labelq2_and_gps', 'nscand_and_labelq2_and_gps',
'far_t_and_nscand_and_labelq2_and_gps']),
# Label criteria met, some additional labels previously added
## FAR = None -------------------------------------
(None, False, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
['labelq2_only']),
(None, False, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
['labelq2_only', 'labelq2_and_gps',]),
(None, True, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
['nscand_and_labelq2', 'labelq2_only']),
(None, True, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
['nscand_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
'nscand_and_labelq2_and_gps']),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
['labelq2_only']),
(DEFAULT_FAR_T*2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
['labelq2_only', 'labelq2_and_gps']),
(DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
['nscand_and_labelq2', 'labelq2_only']),
(DEFAULT_FAR_T*2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
['nscand_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
'nscand_and_labelq2_and_gps']),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
['far_t_and_labelq2', 'labelq2_only']),
(DEFAULT_FAR_T/2.0, False, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
['far_t_and_labelq2', 'labelq2_only', 'labelq2_and_gps',
'far_t_and_labelq2_and_gps']),
(DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL], False,
['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
'far_t_and_nscand_and_labelq2']),
(DEFAULT_FAR_T/2.0, True, 'L7', ['L5', 'L6', RANDOM_LABEL], True,
['far_t_and_labelq2', 'nscand_and_labelq2', 'labelq2_only',
'far_t_and_nscand_and_labelq2', 'labelq2_and_gps',
'far_t_and_labelq2_and_gps', 'nscand_and_labelq2_and_gps',
'far_t_and_nscand_and_labelq2_and_gps']),
# Label criteria previously met -------------------
## FAR = None -------------------------------------
(None, False, RANDOM_LABEL, ['L5', 'L6'], False, []),
(None, False, RANDOM_LABEL, ['L5', 'L6'], True, []),
(None, True, RANDOM_LABEL, ['L5', 'L6'], False, []),
(None, True, RANDOM_LABEL, ['L5', 'L6'], True, []),
## FAR > threshold --------------------------------
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, ['L5', 'L6'], False, []),
(DEFAULT_FAR_T*2.0, False, RANDOM_LABEL, ['L5', 'L6'], True, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, ['L5', 'L6'], False, []),
(DEFAULT_FAR_T*2.0, True, RANDOM_LABEL, ['L5', 'L6'], True, []),
## FAR < threshold --------------------------------
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, ['L5', 'L6'], False, []),
(DEFAULT_FAR_T/2.0, False, RANDOM_LABEL, ['L5', 'L6'], True, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, ['L5', 'L6'], False, []),
(DEFAULT_FAR_T/2.0, True, RANDOM_LABEL, ['L5', 'L6'], True, []),
]
###############################################################################
# TESTS #######################################################################
###############################################################################
# Superevent tests ------------------------------------------------------------
@pytest.mark.parametrize("far,nscand,notif_descs",
SUPEREVENT_CREATION_ALERT_DATA)
@pytest.mark.django_db
def test_superevent_creation_alerts(
superevent, superevent_notifications,
far, nscand, notif_descs,
):
# Set up superevent state
superevent.preferred_event.far = far
superevent.preferred_event.is_ns_candidate = \
types.MethodType(lambda self: nscand, superevent.preferred_event)
# Set up recipient getter
recipient_getter = CreationRecipientGetter(superevent)
recipient_getter.queryset = superevent_notifications
# Get notifications
matched_notifications = recipient_getter.get_notifications()
# Test results
for desc in notif_descs:
assert matched_notifications.filter(description=desc).exists()
assert matched_notifications.count() == len(notif_descs)
@pytest.mark.parametrize("old_far,far,old_nscand,nscand,labels,notif_descs",
SUPEREVENT_UPDATE_ALERT_DATA)
@pytest.mark.django_db
def test_superevent_update_alerts(
superevent, superevent_notifications,
old_far, far, old_nscand, nscand, labels, notif_descs,
):
# Set up superevent state
superevent.preferred_event.far = far
superevent.preferred_event.is_ns_candidate = \
types.MethodType(lambda self: nscand, superevent.preferred_event)
if labels is not None:
for label_name in labels:
label, _ = Label.objects.get_or_create(name=label_name)
superevent.labelling_set.create(label=label,
creator=superevent.submitter)
# Set up recipient getter
recipient_getter = UpdateRecipientGetter(superevent, old_far=old_far,
old_nscand=old_nscand)
recipient_getter.queryset = superevent_notifications
# Get notifications
matched_notifications = recipient_getter.get_notifications()
# Test results
assert matched_notifications.count() == len(notif_descs)
for desc in notif_descs:
assert matched_notifications.filter(description=desc).exists()
@pytest.mark.parametrize("far,nscand,new_label,old_labels,notif_descs",
SUPEREVENT_LABEL_ADDED_ALERT_DATA)
@pytest.mark.django_db
def test_superevent_label_added_alerts(
superevent, superevent_notifications,
far, nscand, new_label, old_labels, notif_descs,
):
# Set up superevent state
superevent.preferred_event.far = far
superevent.preferred_event.is_ns_candidate = \
types.MethodType(lambda self: nscand, superevent.preferred_event)
if old_labels is not None:
for label_name in old_labels:
label, _ = Label.objects.get_or_create(name=label_name)
superevent.labelling_set.create(label=label,
creator=superevent.submitter)
# Add new label
label_obj, _ = Label.objects.get_or_create(name=new_label)
superevent.labelling_set.create(label=label_obj,
creator=superevent.submitter)
# Set up recipient getter
recipient_getter = LabelAddedRecipientGetter(superevent, label=label_obj)
recipient_getter.queryset = superevent_notifications
# Get notifications
matched_notifications = recipient_getter.get_notifications()
# Test results
assert matched_notifications.count() == len(notif_descs)
for desc in notif_descs:
assert matched_notifications.filter(description=desc).exists()
@pytest.mark.parametrize("far,nscand,removed_label,labels,notif_descs",
SUPEREVENT_LABEL_REMOVED_ALERT_DATA)
@pytest.mark.django_db
def test_superevent_label_removed_alerts(
superevent, superevent_notifications,
far, nscand, removed_label, labels, notif_descs,
):
# NOTE: labels being removed should never result in alerts for
# notifications which specify a label criteria (instead of a label query)
# Set up superevent state
superevent.preferred_event.far = far
superevent.preferred_event.is_ns_candidate = \
types.MethodType(lambda self: nscand, superevent.preferred_event)
if labels is not None:
for label_name in labels:
label, _ = Label.objects.get_or_create(name=label_name)
superevent.labelling_set.create(label=label,
creator=superevent.submitter)
# Add new label
label_obj, _ = Label.objects.get_or_create(name=removed_label)
# Set up recipient getter
recipient_getter = LabelRemovedRecipientGetter(superevent, label=label_obj)
recipient_getter.queryset = superevent_notifications
# Get notifications
matched_notifications = recipient_getter.get_notifications()
# Test results
assert matched_notifications.count() == len(notif_descs)
for desc in notif_descs:
assert matched_notifications.filter(description=desc).exists()
# Event tests -----------------------------------------------------------------
@pytest.mark.parametrize("far,nscand,use_default_gps,notif_descs",
EVENT_CREATION_ALERT_DATA)
@pytest.mark.django_db
def test_event_creation_alerts(
event, event_notifications, far, nscand, use_default_gps, notif_descs,
):
# Set up event state
event.far = far
event.is_ns_candidate = types.MethodType(lambda self: nscand, event)
if use_default_gps:
event.group = Group.objects.get(name=DEFAULT_GROUP)
event.pipeline = Pipeline.objects.get(name=DEFAULT_PIPELINE)
event.search = Search.objects.get(name=DEFAULT_SEARCH)
event.save()
# Set up recipient getter
recipient_getter = CreationRecipientGetter(event)
recipient_getter.queryset = event_notifications
# Get notifications
matched_notifications = recipient_getter.get_notifications()
# Test results
for desc in notif_descs:
assert matched_notifications.filter(description=desc).exists()
assert matched_notifications.count() == len(notif_descs)
@pytest.mark.parametrize(
"old_far,far,old_nscand,nscand,use_default_gps,labels,notif_descs",
EVENT_UPDATE_ALERT_DATA
)
@pytest.mark.django_db
def test_event_update_alerts(
event, event_notifications, old_far, far, old_nscand, nscand,
use_default_gps, labels, notif_descs,
):
# Set up event state
event.far = far
event.is_ns_candidate = types.MethodType(lambda self: nscand, event)
if use_default_gps:
event.group = Group.objects.get(name=DEFAULT_GROUP)
event.pipeline = Pipeline.objects.get(name=DEFAULT_PIPELINE)
event.search = Search.objects.get(name=DEFAULT_SEARCH)
event.save()
if labels is not None:
for label_name in labels:
label, _ = Label.objects.get_or_create(name=label_name)
event.labelling_set.create(label=label, creator=event.submitter)
# Set up recipient getter
recipient_getter = UpdateRecipientGetter(event, old_far=old_far,
old_nscand=old_nscand)
recipient_getter.queryset = event_notifications
# Get notifications
matched_notifications = recipient_getter.get_notifications()
# Test results
assert matched_notifications.count() == len(notif_descs)
for desc in notif_descs:
assert matched_notifications.filter(description=desc).exists()
@pytest.mark.parametrize(
"far,nscand,new_label,old_labels,use_default_gps,notif_descs",
EVENT_LABEL_ADDED_ALERT_DATA
)
@pytest.mark.django_db
def test_event_label_added_alerts(
event, event_notifications, far, nscand, new_label, old_labels,
use_default_gps, notif_descs,
):
# Set up event state
event.far = far
event.is_ns_candidate = types.MethodType(lambda self: nscand, event)
if use_default_gps:
event.group = Group.objects.get(name=DEFAULT_GROUP)
event.pipeline = Pipeline.objects.get(name=DEFAULT_PIPELINE)
event.search = Search.objects.get(name=DEFAULT_SEARCH)
event.save()
if old_labels is not None:
for label_name in old_labels:
label, _ = Label.objects.get_or_create(name=label_name)
event.labelling_set.create(label=label, creator=event.submitter)
# Add new label
label_obj, _ = Label.objects.get_or_create(name=new_label)
event.labelling_set.create(label=label_obj, creator=event.submitter)
# Set up recipient getter
recipient_getter = LabelAddedRecipientGetter(event, label=label_obj)
recipient_getter.queryset = event_notifications
# Get notifications
matched_notifications = recipient_getter.get_notifications()
# Test results
assert matched_notifications.count() == len(notif_descs)
for desc in notif_descs:
assert matched_notifications.filter(description=desc).exists()
@pytest.mark.parametrize(
"far,nscand,removed_label,labels,use_default_gps,notif_descs",
EVENT_LABEL_REMOVED_ALERT_DATA
)
@pytest.mark.django_db
def test_event_label_removed_alerts(
event, event_notifications, far, nscand, removed_label, labels,
use_default_gps, notif_descs,
):
# Set up event state
event.far = far
event.is_ns_candidate = types.MethodType(lambda self: nscand, event)
if use_default_gps:
event.group = Group.objects.get(name=DEFAULT_GROUP)
event.pipeline = Pipeline.objects.get(name=DEFAULT_PIPELINE)
event.search = Search.objects.get(name=DEFAULT_SEARCH)
event.save()
if labels is not None:
for label_name in labels:
label, _ = Label.objects.get_or_create(name=label_name)
event.labelling_set.create(label=label, creator=event.submitter)
# Add new label
label_obj, _ = Label.objects.get_or_create(name=removed_label)
# Set up recipient getter
recipient_getter = LabelRemovedRecipientGetter(event, label=label_obj)
recipient_getter.queryset = event_notifications
# Get notifications
matched_notifications = recipient_getter.get_notifications()
# Test results
assert matched_notifications.count() == len(notif_descs)
for desc in notif_descs:
assert matched_notifications.filter(description=desc).exists()
# Other tests -----------------------------------------------------------------
@pytest.mark.django_db
def test_complex_label_query(superevent):
# NOTE: L1 & ~L2 | L3 == (L1 & ~L2) | L3
n = Notification.objects.create(
label_query='L1 & ~L2 | L3',
user=superevent.submitter,
description='test notification'
)
# Set up superevent labels
for label_name in ['L1', 'L2', 'L3']:
l, _ = Label.objects.get_or_create(name=label_name)
n.labels.add(l)
# Test label added recipients for L1 being added
l1 = Label.objects.get(name='L1')
superevent.labelling_set.create(creator=superevent.submitter, label=l1)
recipient_getter = LabelAddedRecipientGetter(superevent, label=l1)
matched_notifications = recipient_getter.get_notifications()
assert matched_notifications.count() == 1
assert matched_notifications.first().description == n.description
# Test label added recipients for L3 being added (L1 already added)
l3 = Label.objects.get(name='L3')
superevent.labelling_set.create(creator=superevent.submitter, label=l3)
recipient_getter = LabelAddedRecipientGetter(superevent, label=l3)
matched_notifications = recipient_getter.get_notifications()
assert matched_notifications.count() == 1
assert matched_notifications.first().description == n.description
# Test label added recipients for L3 being added (no L1)
l3 = Label.objects.get(name='L3')
superevent.labels.clear()
superevent.labelling_set.create(creator=superevent.submitter, label=l3)
recipient_getter = LabelAddedRecipientGetter(superevent, label=l3)
matched_notifications = recipient_getter.get_notifications()
assert matched_notifications.count() == 1
assert matched_notifications.first().description == n.description
# Test label added recipients for L1 being added (with L2)
l1 = Label.objects.get(name='L1')
l2 = Label.objects.get(name='L2')
superevent.labels.clear()
superevent.labelling_set.create(creator=superevent.submitter, label=l2)
superevent.labelling_set.create(creator=superevent.submitter, label=l1)
recipient_getter = LabelAddedRecipientGetter(superevent, label=l1)
matched_notifications = recipient_getter.get_notifications()
assert matched_notifications.count() == 0
# Test label added recipients for L3 being added (with L1 and L2)
l1 = Label.objects.get(name='L1')
l2 = Label.objects.get(name='L2')
l3 = Label.objects.get(name='L3')
superevent.labels.clear()
superevent.labelling_set.create(creator=superevent.submitter, label=l1)
superevent.labelling_set.create(creator=superevent.submitter, label=l2)
superevent.labelling_set.create(creator=superevent.submitter, label=l3)
recipient_getter = LabelAddedRecipientGetter(superevent, label=l3)
matched_notifications = recipient_getter.get_notifications()
assert matched_notifications.count() == 1
assert matched_notifications.first().description == n.description
# Test label removed recipients for L2 being removed (with L1)
l1 = Label.objects.get(name='L1')
l2 = Label.objects.get(name='L2')
superevent.labels.clear()
superevent.labelling_set.create(creator=superevent.submitter, label=l1)
recipient_getter = LabelRemovedRecipientGetter(superevent, label=l2)
matched_notifications = recipient_getter.get_notifications()
assert matched_notifications.count() == 1
assert matched_notifications.first().description == n.description
# Test label removed recipients for L3 being removed (with L1)
l1 = Label.objects.get(name='L1')
l3 = Label.objects.get(name='L3')
superevent.labels.clear()
superevent.labelling_set.create(creator=superevent.submitter, label=l1)
recipient_getter = LabelRemovedRecipientGetter(superevent, label=l3)
matched_notifications = recipient_getter.get_notifications()
assert matched_notifications.count() == 1
assert matched_notifications.first().description == n.description
# Test label removed recipients for L2 being removed (with L1 and L3)
l1 = Label.objects.get(name='L1')
l2 = Label.objects.get(name='L2')
l3 = Label.objects.get(name='L3')
superevent.labels.clear()
superevent.labelling_set.create(creator=superevent.submitter, label=l1)
superevent.labelling_set.create(creator=superevent.submitter, label=l3)
recipient_getter = LabelAddedRecipientGetter(superevent, label=l2)
matched_notifications = recipient_getter.get_notifications()
# TODO: this shouldn't match, since it was already triggered on the
# previous state, where L1, L2, and L3 were all applied (but it does)
#assert matched_notifications.count() == 0
@pytest.mark.django_db
def test_label_removal_with_only_labels_list(superevent):
n = Notification.objects.create(
user=superevent.submitter,
description='test notification'
)
# Set up superevent labels
for label_name in ['L1', 'L2']:
l, _ = Label.objects.get_or_create(name=label_name)
n.labels.add(l)
# Test label added recipients for L1 being added
l1 = Label.objects.get(name='L1')
l2 = Label.objects.get(name='L2')
l3, _ = Label.objects.get_or_create(name='L3')
superevent.labelling_set.create(creator=superevent.submitter, label=l1)
superevent.labelling_set.create(creator=superevent.submitter, label=l2)
recipient_getter = LabelRemovedRecipientGetter(superevent, label=l3)
matched_notifications = recipient_getter.get_notifications()
assert matched_notifications.count() == 0
@pytest.mark.parametrize("old_far,new_far,far_t,match",
[(1, 0.5, 0.5, False), (0.5, 0.25, 0.5, True)])
@pytest.mark.django_db
def test_update_alert_far_threshold_edge(superevent, old_far, new_far, far_t,
match):
n = Notification.objects.create(
user=superevent.submitter,
far_threshold=far_t,
description='test notification'
)
# Set up superevent state
superevent.preferred_event.far = new_far
# Set up recipient getter
recipient_getter = UpdateRecipientGetter(superevent, old_far=old_far,
old_nscand=False)
# Get notifications
matched_notifications = recipient_getter.get_notifications()
if match:
assert matched_notifications.count() == 1
assert matched_notifications.first().description == n.description
else:
assert matched_notifications.count() == 0
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
from django.urls import reverse
from core.tests.utils import GraceDbTestBase
from alerts.models import Contact, Notification
class TestUpdateContactView(GraceDbTestBase):
@classmethod
def setUpTestData(cls):
super(TestUpdateContactView, cls).setUpTestData()
# Create email and phone contacts
cls.email_contact = Contact.objects.create(user=cls.internal_user,
description='test email', email='test@test.com')
cls.phone_contact = Contact.objects.create(user=cls.internal_user,
description='test phone', phone='12345678901',
phone_method=Contact.CONTACT_PHONE_BOTH)
# Refresh from database to get formatted phone numbers
cls.email_contact.refresh_from_db()
cls.phone_contact.refresh_from_db()
def test_edit_email(self):
"""Users should not be able to update contact email"""
# (because it sidesteps the verification process)
data = {
'key_field': 'email',
'description': 'new description',
'email': 'new@new.com',
}
original_email = self.email_contact.email
url = reverse('alerts:edit-contact', args=[self.email_contact.pk])
response = self.request_as_user(url, "POST", self.internal_user,
data=data)
# Response = 302 means success and redirect to main alerts page
self.assertEqual(response.status_code, 302)
# Refresh from database
self.email_contact.refresh_from_db()
# Check values - description should be updated, but email should not be
self.assertEqual(self.email_contact.description, data['description'])
self.assertNotEqual(self.email_contact.email, data['email'])
self.assertEqual(self.email_contact.email, original_email)
def test_edit_phone(self):
"""Users should not be able to update contact phone"""
# (because it sidesteps the verification process)
data = {
'key_field': 'phone',
'description': 'new description',
'phone': '23456789012',
'phone_method': Contact.CONTACT_PHONE_CALL,
}
original_phone = self.phone_contact.phone
url = reverse('alerts:edit-contact', args=[self.phone_contact.pk])
response = self.request_as_user(url, "POST", self.internal_user,
data=data)
# Response = 302 means success and redirect to main alerts page
self.assertEqual(response.status_code, 302)
# Refresh from database
self.phone_contact.refresh_from_db()
# Check values - description and method should be updated, but phone
# number should not be
self.assertEqual(self.phone_contact.description, data['description'])
self.assertNotEqual(self.phone_contact.phone, data['phone'])
self.assertEqual(self.phone_contact.phone, original_phone)
self.assertEqual(self.phone_contact.phone_method, data['phone_method'])
@pytest.mark.skip(reason="Broke when upgrading django 2.2 -> 3.2, not sure why")
@pytest.mark.parametrize("notif_exists", [True, False])
@pytest.mark.django_db
def test_delete_contact(notif_exists, internal_user, client):
client.force_login(internal_user)
# Manually create verified contact for user
contact = internal_user.contact_set.create(phone='12345678901',
verified=True, phone_method=Contact.CONTACT_PHONE_TEXT,
description='test')
# Optionally create a notification
if notif_exists:
notif = internal_user.notification_set.create(description='test',
category=Notification.NOTIFICATION_CATEGORY_SUPEREVENT)
notif.contacts.add(contact)
# Try to delete contact
response = client.get(reverse('alerts:delete-contact', args=[contact.pk]))
# Assert results
if notif_exists:
# Redirect to main alerts page
assert response.status_code == 302
assert response.url == reverse('alerts:index')
# Message displayed by message framework
assert "cannot be deleted" in response.cookies['messages'].value
# Contact still exists
assert Contact.objects.filter(pk=contact.pk).exists()
else:
# Redirect to main alerts page
assert response.status_code == 302
assert response.url == reverse('alerts:index')
# Contact does not exist
assert not Contact.objects.filter(pk=contact.pk).exists()
@pytest.mark.django_db
def test_create_notification_no_contact(internal_user, client):
client.force_login(internal_user)
# Manually create verified contact for user
contact = internal_user.contact_set.create(phone='12345678901',
verified=True, phone_method=Contact.CONTACT_PHONE_TEXT,
description='test')
# Try to create a notification with no contact
url = reverse('alerts:create-notification')
data = {
'description': 'test',
'key_field': 'superevent',
}
response = client.post(url, data=data)
# This should call the forms_invalid() method of the CBV,
# resulting in a TemplateResponse to render the form with
# errors
# Get form
form = [form for form in response.context['forms']
if form.key == 'superevent'][0]
# Check form status and errors
assert form.is_bound == True
assert form.is_valid() == False
assert 'contacts' in form.errors
assert form.errors.as_data()['contacts'][0].code == 'required'
@mock.patch('alerts.models.twilio_client')
@mock.patch('alerts.models.get_twilio_from', lambda: '12345678901')
class TestRequestVerificationCode(GraceDbTestBase):
def helper_test_verification_process(self, mock_twilio_client,
**contact_kwargs):
"""Creates a Contact, requests and submits a verification code"""
self.client.force_login(self.internal_user)
# Create an unverified Contact
contact = self.internal_user.contact_set.create(description='test',
**contact_kwargs)
# Request the verification code
url = reverse('alerts:request-verification-code', args=(contact.id,))
response = self.client.get(url)
# Redirect to verify contact page
assert response.status_code == 302
assert response.url == reverse('alerts:verify-contact', args=(contact.id,))
# Update the Contact so the verification code is accessible
contact.refresh_from_db()
verification_code = contact.verification_code
# Assert that the Contact is not yet verified
assert not contact.verified
# Submit the verification code
url = reverse('alerts:verify-contact', args=(contact.id,))
data = {
'code': verification_code,
}
response = self.client.post(url, data=data)
# Redirect to index
assert response.status_code == 302
assert response.url == reverse('alerts:index')
# Assert that the Contact is now verified
contact.refresh_from_db()
assert contact.verified
def test_verification_process_phone_call(self, mock_twilio_client):
self.helper_test_verification_process(mock_twilio_client,
phone_method=Contact.CONTACT_PHONE_CALL, phone='12345678901')
def test_verification_process_phone_text(self, mock_twilio_client):
self.helper_test_verification_process(mock_twilio_client,
phone_method=Contact.CONTACT_PHONE_TEXT, phone='12345678901')
def test_verification_process_phone_text(self, mock_twilio_client):
self.helper_test_verification_process(mock_twilio_client,
phone_method=Contact.CONTACT_PHONE_BOTH, phone='12345678901')
def test_verification_process_email(self, mock_twilio_client):
self.helper_test_verification_process(mock_twilio_client,
email='albert.einstein@ligo.org')
def helper_test_contact_already_verified(self, mock_twilio_client,
**contact_kwargs):
"""Tests that verification code not sent for verified Contact"""
self.client.force_login(self.internal_user)
# Create an unverified Contact
contact = self.internal_user.contact_set.create(description='test',
verified=True, **contact_kwargs)
# Request the verification code
url = reverse('alerts:request-verification-code', args=(contact.id,))
response = self.client.get(url)
# Redirect to index
assert response.status_code == 302
assert response.url == reverse('alerts:index')
# No message should have been sent
mock_twilio_client.assert_not_called()
def test_contact_already_verified_phone_call(self, mock_twilio_client):
self.helper_test_contact_already_verified(mock_twilio_client,
phone_method=Contact.CONTACT_PHONE_CALL, phone='12345678901')
def test_contact_already_verified_phone_text(self, mock_twilio_client):
self.helper_test_contact_already_verified(mock_twilio_client,
phone_method=Contact.CONTACT_PHONE_TEXT, phone='12345678901')
def test_contact_already_verified_phone_both(self, mock_twilio_client):
self.helper_test_contact_already_verified(mock_twilio_client,
phone_method=Contact.CONTACT_PHONE_BOTH, phone='12345678901')
def test_contact_already_verified_email(self, mock_twilio_client):
self.helper_test_contact_already_verified(mock_twilio_client,
email='albert.einstein@ligo.org')
from django.urls import re_path
from . import views
app_name = 'alerts'
urlpatterns = [
# Base /options/ URL
re_path(r'^$', views.index, name="index"),
# Contacts
re_path(r'^contact/create/', views.CreateContactView.as_view(),
name='create-contact'),
re_path(r'^contact/(?P<pk>\d+)/edit/$', views.EditContactView.as_view(),
name="edit-contact"),
re_path(r'^contact/(?P<pk>\d+)/delete/$', views.DeleteContactView.as_view(),
name="delete-contact"),
re_path(r'^contact/(?P<pk>\d+)/test/$', views.TestContactView.as_view(),
name="test-contact"),
re_path(r'^contact/(?P<pk>\d+)/request-code/$',
views.RequestVerificationCodeView.as_view(),
name="request-verification-code"),
re_path(r'^contact/(?P<pk>\d+)/verify/$', views.VerifyContactView.as_view(),
name="verify-contact"),
# Notifications
re_path(r'^notification/create/$', views.CreateNotificationView.as_view(),
name="create-notification"),
re_path(r'^notification/(?P<pk>\d+)/edit/$',
views.EditNotificationView.as_view(), name="edit-notification"),
re_path(r'^notification/(?P<pk>\d+)/delete/$',
views.DeleteNotificationView.as_view(), name="delete-notification"),
]
from __future__ import absolute_import
from pyparsing import (
oneOf, Optional, ZeroOrMore, StringEnd, Suppress, infixNotation, opAssoc,
)
import re
from events.models import Label
from .constants import OPERATORS
def get_label_parser():
return oneOf(list(Label.objects.values_list('name', flat=True)))
def parse_label_query(s, keep_binary_ops=False):
"""
Parses a label query into a list. The output is a list whose elements
depend on the options:
>>> parse_label_query('A & B', keep_binary_ops=False)
['A', 'B']
>>> parse_label_query('A & B', keep_binary_ops=True)
['A', '&', 'B']
"""
# Parser for one label name
label = get_label_parser()
# "intermediate" parser - between labels should be AND or OR and then
# an optional NOT
im = (OPERATORS['AND'] ^ OPERATORS['OR']) + Optional(OPERATORS['NOT'])
if not keep_binary_ops:
im = Suppress(im)
# Full parser: optional NOT and a label, then zero or more
# "intermediate" + label combos, then string end
optional_initial_not = Optional(OPERATORS['NOT'])
if not keep_binary_ops:
optional_initial_not = Suppress(optional_initial_not)
label_expr = optional_initial_not + label + \
ZeroOrMore(im + label) + StringEnd()
return label_expr.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
def get_label_query_parser(label_name_list):
label_parser = get_label_parser()
label_parser.setParseAction(lambda toks: toks[0] in label_name_list)
label_query_parser = infixNotation(label_parser,
[
(OPERATORS['NOT'], 1, opAssoc.RIGHT, lambda toks: not toks[0][1]),
(OPERATORS['AND'], 2, opAssoc.LEFT, lambda toks: all(toks[0][0::2])),
(OPERATORS['OR'], 2, opAssoc.LEFT, lambda toks: any(toks[0][0::2])),
]
)
return label_query_parser
def evaluate_label_queries(label_name_list, notifications):
"""
Takes in a list of label names and a queryset of Notifications,
returns pks of Notifications which have a label_query and that
label_query is evaluates to True
"""
label_query_parser = get_label_query_parser(label_name_list)
notification_pks = []
for n in notifications.filter(label_query__isnull=False):
valid = label_query_parser.parseString(n.label_query)[0]
if valid:
notification_pks.append(n.pk)
return notification_pks
import phonenumbers
from django.core.exceptions import ValidationError
def validate_phone(value):
# Try to parse phone number
try:
phone = phonenumbers.parse(value, 'US')
except phonenumbers.NumberParseException:
raise ValidationError('Not a valid phone number: {0}'.format(value))
# Validate phone number
if not phonenumbers.is_valid_number(phone):
raise ValidationError('Not a valid phone number: {0}'.format(value))
import logging
from django.conf import settings
from django.contrib import messages
from django.core.mail import EmailMessage
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.generic.edit import DeleteView, UpdateView
from django.views.generic.detail import DetailView
from django_twilio.client import twilio_client
from core.views import MultipleFormView
from ligoauth.decorators import internal_user_required
from .forms import (
PhoneContactForm, EmailContactForm, VerifyContactForm,
EventNotificationForm, SupereventNotificationForm,
)
from . import egad
from .models import Contact, Notification
from .phone import get_twilio_from
# Set up logger
logger = logging.getLogger(__name__)
###############################################################################
# Generic views ###############################################################
###############################################################################
@internal_user_required
def index(request):
context = {
'notifications': request.user.notification_set.all(),
'contacts': request.user.contact_set.all(),
}
return render(request, 'alerts/index.html', context=context)
###############################################################################
# Notification views ##########################################################
###############################################################################
@method_decorator(internal_user_required, name='dispatch')
class CreateNotificationView(MultipleFormView):
"""Create a notification"""
template_name = 'alerts/create_notification.html'
success_url = reverse_lazy('alerts:index')
form_classes = [SupereventNotificationForm, EventNotificationForm]
def get_context_data(self, **kwargs):
kwargs['idx'] = 0
if (self.request.method in ('POST', 'PUT')):
form_keys = [f.key for f in self.form_classes]
idx = form_keys.index(self.request.POST['key_field'])
kwargs['idx'] = idx
return kwargs
def get_form_kwargs(self, *args, **kwargs):
kw = super(CreateNotificationView, self).get_form_kwargs(
*args, **kwargs)
kw['user'] = self.request.user
return kw
def form_valid(self, form):
form.cleaned_data.pop('key_field', None)
# Add user (from request) and category (stored on form class) to
# the form instance, then save
form.instance.user = self.request.user
form.instance.category = form.category
form.save()
# Add message and return
messages.info(self.request, 'Created notification: {n}.'.format(
n=form.instance.description))
return super(CreateNotificationView, self).form_valid(form)
def get(self, request, *args, **kwargs):
# Make sure user has at least one verified contact; if not, redirect
# and display an error message
user_has_verified_contact = request.user.contact_set.filter(
verified=True).exists()
if not user_has_verified_contact:
messages.error(request, ('Error: you have no verified contacts. '
'Create and verify a contact before creating a notification.'))
return HttpResponseRedirect(reverse('alerts:index'))
return super(CreateNotificationView, self).get(request, *args,
**kwargs)
superevent_form_valid = event_form_valid = form_valid
@method_decorator(internal_user_required, name='dispatch')
class EditNotificationView(UpdateView):
"""Edit a notification"""
template_name = 'alerts/edit_notification.html'
# Have to provide form_class, but it will be dynamically selected below in
# get_form()
form_class = SupereventNotificationForm
success_url = reverse_lazy('alerts:index')
def get_form_class(self):
if self.object.category == Notification.NOTIFICATION_CATEGORY_EVENT:
return EventNotificationForm
else:
return SupereventNotificationForm
def get_form_kwargs(self, *args, **kwargs):
kw = super(EditNotificationView, self).get_form_kwargs(
*args, **kwargs)
kw['user'] = self.request.user
# Cases that have a label query actually have labels in the database.
# But we don't want to include those in the form because
# a) it's confusing and b) it breaks the form
if self.object.label_query and self.object.labels.exists():
kw['initial']['labels'] = None
return kw
def get_queryset(self):
return self.request.user.notification_set.all()
@method_decorator(internal_user_required, name='dispatch')
class DeleteNotificationView(DeleteView):
"""Delete a notification"""
success_url = reverse_lazy('alerts:index')
def get(self, request, *args, **kwargs):
# Override this so that we don't require a confirmation page
# for deletion
return self.delete(request, *args, **kwargs)
def form_valid(self, form, request, *args, **kwargs):
response = super(DeleteNotificationView, self).delete(request, *args,
**kwargs)
messages.info(request, 'Notification "{n}" has been deleted.'.format(
n=self.object.description))
return super().delete(request, *args, **kwargs)
def get_queryset(self):
# Queryset should only contain the user's notifications
return self.request.user.notification_set.all()
###############################################################################
# Contact views ###############################################################
###############################################################################
@method_decorator(internal_user_required, name='dispatch')
class CreateContactView(MultipleFormView):
"""Create a contact"""
template_name = 'alerts/create_contact.html'
success_url = reverse_lazy('alerts:index')
form_classes = [PhoneContactForm, EmailContactForm]
def get_context_data(self, **kwargs):
kwargs['idx'] = 0
if (self.request.method in ('POST', 'PUT')):
form_keys = [f.key for f in self.form_classes]
idx = form_keys.index(self.request.POST['key_field'])
kwargs['idx'] = idx
return kwargs
def form_valid(self, form):
# Remove key_field, add user, and save form
form.cleaned_data.pop('key_field', None)
form.instance.user = self.request.user
form.save()
# Generate message and return
messages.info(self.request, 'Created contact "{cname}".'.format(
cname=form.instance.description))
return super(CreateContactView, self).form_valid(form)
email_form_valid = phone_form_valid = form_valid
@method_decorator(internal_user_required, name='dispatch')
class EditContactView(UpdateView):
"""
Edit a contact. Users shouldn't be able to edit the actual email address
or phone number since that would allow them to circumvent the verification
process.
"""
template_name = 'alerts/edit_contact.html'
# Have to provide form_class, but it will be dynamically selected below in
# get_form()
form_class = PhoneContactForm
success_url = reverse_lazy('alerts:index')
def get_form_class(self):
if self.object.phone is not None:
return PhoneContactForm
else:
return EmailContactForm
return self.form_class
def get_form(self, form_class=None):
form = super(EditContactView, self).get_form(form_class)
if isinstance(form, PhoneContactForm):
form.fields['phone'].disabled = True
elif isinstance(form, EmailContactForm):
form.fields['email'].disabled = True
return form
def get_queryset(self):
return self.request.user.contact_set.all()
@method_decorator(internal_user_required, name='dispatch')
class DeleteContactView(DeleteView):
"""Delete a contact"""
success_url = reverse_lazy('alerts:index')
def get(self, request, *args, **kwargs):
# Override this so that we don't require a confirmation page
# for deletion
return self.delete(request, *args, **kwargs)
def form_valid(self, form, request, *args, **kwargs):
# Get contact
self.object = self.get_object()
# Contacts can only be deleted if they aren't part of a notification -
# this will prevent cases where a user creates a notification, deletes
# the related contact(s), and then wonders why they aren't getting
# any notifications.
if self.object.notification_set.exists():
messages.error(request, ('Contact "{cname}" cannot be deleted '
'because it is part of a notification. Remove it from the '
'notification or delete the notification first.').format(
cname=self.object.description))
return HttpResponseRedirect(reverse('alerts:index'))
# Otherwise, delete the contact and show a corresponding message.
self.object.is_deleted = True
self.object.delete()
messages.info(request, 'Contact "{cname}" has been deleted.'.format(
cname=self.object.description))
return HttpResponseRedirect(self.get_success_url())
def get_queryset(self):
# Queryset should only contain the user's contacts
return self.request.user.contact_set.all()
@method_decorator(internal_user_required, name='dispatch')
class TestContactView(DetailView):
"""Test a contact (must be verified already)"""
# Send alerts to all contact methods
success_url = reverse_lazy('alerts:index')
def get_queryset(self):
return self.request.user.contact_set.all()
def get(self, request, *args, **kwargs):
self.object = self.get_object()
# Handle case where contact is not verified
if not self.object.verified:
msg = ('Contact "{desc}" must be verified before it can be '
'tested.').format(desc=self.object.description)
messages.info(request, msg)
return HttpResponseRedirect(self.success_url)
# Send test notifications
msg = 'This is a test of contact "{desc}" from {host}.'.format(
desc=self.object.description, host=settings.LIGO_FQDN)
if self.object.email:
subject = 'Test of contact "{desc}" from {host}'.format(
desc=self.object.description, host=settings.LIGO_FQDN)
if settings.ENABLE_EGAD_EMAIL:
payload = {
"recipients": [self.object.email],
"subject": subject,
"body": msg,
}
egad.send_alert("email", payload)
else:
email = EmailMessage(subject, msg,
from_email=settings.ALERT_EMAIL_FROM, to=[self.object.email])
email.send()
if self.object.phone:
# Construct URL of TwiML bin
twiml_url = '{base}{twiml_bin}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['test'])
if settings.ENABLE_EGAD_PHONE:
payload = {
"contacts": [{
"phone_method": self.object.phone_method,
"phone_number": self.object.phone,
}],
"message": msg,
"twiml_url": twiml_url,
}
egad.send_alert("phone", payload)
else:
# Get "from" phone number.
from_ = get_twilio_from()
# Send test call
if (self.object.phone_method == Contact.CONTACT_PHONE_CALL or
self.object.phone_method == Contact.CONTACT_PHONE_BOTH):
# Make call
twilio_client.calls.create(to=self.object.phone, from_=from_,
url=twiml_url, method='GET')
if (self.object.phone_method == Contact.CONTACT_PHONE_TEXT or
self.object.phone_method == Contact.CONTACT_PHONE_BOTH):
twilio_client.messages.create(to=self.object.phone,
from_=from_, body=msg)
# Message for web view
messages.info(request, 'Testing contact "{desc}".'.format(
desc=self.object.description))
return HttpResponseRedirect(self.success_url)
@method_decorator(internal_user_required, name='dispatch')
class VerifyContactView(UpdateView):
"""Request a verification code or verify a contact"""
template_name = 'alerts/verify_contact.html'
form_class = VerifyContactForm
success_url = reverse_lazy('alerts:index')
def get_queryset(self):
return self.request.user.contact_set.all()
def form_valid(self, form):
self.object.verify()
msg = 'Contact "{cname}" successfully verified.'.format(
cname=self.object.description)
messages.info(self.request, msg)
return super(VerifyContactView, self).form_valid(form)
def get_context_data(self, **kwargs):
context = super(VerifyContactView, self).get_context_data(**kwargs)
# Determine if verification code exists and is expired
if (self.object.verification_code is not None and
timezone.now() > self.object.verification_expiration):
context['code_expired'] = True
return context
@method_decorator(internal_user_required, name='dispatch')
class RequestVerificationCodeView(DetailView):
"""Redirect view for requesting a contact verification code"""
def get_queryset(self):
return self.request.user.contact_set.all()
def get(self, request, *args, **kwargs):
self.object = self.get_object()
# Handle case where contact is already verified
if self.object.verified:
msg = 'Contact "{desc}" is already verified.'.format(
desc=self.object.description)
messages.info(request, msg)
return HttpResponseRedirect(reverse('alerts:index'))
# Otherwise, set up verification code for contact
self.object.generate_verification_code()
# Send verification code
self.object.send_verification_code()
messages.info(request, "Verification code sent.")
return HttpResponseRedirect(reverse('alerts:verify-contact',
args=[self.object.pk]))
from __future__ import absolute_import
import copy
import logging
import os
import simplejson
import socket
import sys
from django.core.mail import EmailMessage
from django.conf import settings
from xml.sax.saxutils import escape
from datetime import datetime, timezone
from hashlib import sha1
from core.time_utils import gpsToUtc
from events.permission_utils import is_external
from events.shortcuts import is_event
from superevents.shortcuts import is_superevent
from .lvalert import send_with_lvalert_overseer, send_with_kafka_client
from . import egad
# Set up logger
logger = logging.getLogger(__name__)
def get_xmpp_node_names(event_or_superevent):
"""
Utility function for determining the names of nodes to which XMPP
notifications should be sent. Accepts an event or superevent object as the
sole argument.
"""
# Compile a list of node names
node_names = []
if is_superevent(event_or_superevent):
superevent = event_or_superevent
if superevent.is_production():
superevent_node = 'superevent'
elif superevent.is_mdc():
superevent_node = 'mdc_superevent'
else:
superevent_node = 'test_superevent'
node_names.append(superevent_node)
elif is_event(event_or_superevent):
# Node name format is group_pipeline or group_pipeline_search
# If search is provided, we send alerts to both of the relevant nodes
# NOTE: for test events, group=Test
event = event_or_superevent
gp_node = "{group}_{pipeline}".format(group=event.group.name,
pipeline=event.pipeline.name).lower()
node_names.append(gp_node)
if event.search and settings.SEND_TO_SEARCH_TOPICS:
gps_node = gp_node + "_{search}".format(
search=event.search.name.lower())
node_names.append(gps_node)
else:
error_msg = ('Object is of {0} type; should be an event '
'or superevent').format(type(event_or_superevent))
logger.error(error_msg)
# TODO: way to catch this?
raise TypeError(error_msg)
return node_names
def issue_xmpp_alerts_local(event_or_superevent, alert_type, serialized_object,
serialized_parent=None):
"""
serialized_object should be a dict
"""
# Check settings switch for turning off XMPP alerts
if not settings.SEND_XMPP_ALERTS:
return
# FIXME: quarantine detchar and hardwareinjection events for now
if (is_event(event_or_superevent) and
(event_or_superevent.group.name == 'Detchar' or
event_or_superevent.pipeline.name == 'HardwareInjection')):
return
# Determine LVAlert node names
node_names = get_xmpp_node_names(event_or_superevent)
# Get uid
uid = event_or_superevent.graceid
# Create the output dictionary and serialize as JSON.
lva_data = {
'uid': uid,
'alert_type': alert_type,
'dispatched': f'{datetime.now(timezone.utc):%Y-%m-%d %H:%M:%S %Z}',
'data': serialized_object,
}
# Add serialized "parent" object
if serialized_parent is not None:
lva_data['object'] = serialized_parent
# Dump to JSON format:
# simplejson.dumps is needed to properly handle Decimal fields
msg = simplejson.dumps(lva_data)
# Try 'escaping' the message:
msg = escape(msg)
# Log message for debugging
logger.info("issue_xmpp_alerts: sending message {msg} for {uid}" \
.format(msg=msg, uid=uid))
# Loop over LVAlert servers and nodes, issuing the alert to each
for overseer_instance in settings.LVALERT_OVERSEER_INSTANCES[::-1]:
server = overseer_instance.get('lvalert_server')
port = overseer_instance.get('listen_port')
for node_name in node_names:
# Calculate unique message_id and log
message_id = sha1((node_name + msg).encode()).hexdigest()
# Log message
logger.info(("issue_kafka_alerts: sending alert type {alert_type} "
"with message {msg_id} for {uid} to {node} on {server}").format(
alert_type=alert_type, msg_id=message_id, uid=uid,
node=node_name, server=server))
# Try to send with LVAlert Overseer (if enabled)
success = False
if settings.USE_LVALERT_OVERSEER:
# Send with LVAlert Overseer
success = send_with_lvalert_overseer(node_name, msg, port)
# If not success, we need to do this the old way.
if not success:
logger.critical(("issue_kafka_alerts: sending message with "
"Overseer failed, trying igwn-alert client code"))
# If not using Overseer or if sending with overseer failed,
# use basic igwn-alert client send
if (not settings.USE_LVALERT_OVERSEER) or (not success):
try:
# Make a settings dictionary and then change some names:
lvalert_settings_dict = copy.deepcopy(overseer_instance)
port = lvalert_settings_dict.pop('listen_port')
server = lvalert_settings_dict.pop('lvalert_server')
lvalert_settings_dict['group'] = lvalert_settings_dict.pop('igwn_alert_group')
send_with_kafka_client(node_name, msg, server,
**lvalert_settings_dict)
except Exception as e:
logger.critical(("issue_kafka_alerts: error sending "
"message with igwn-alert client: {e}").format(e=e))
def issue_xmpp_alerts_egad(event_or_superevent, alert_type, serialized_object,
serialized_parent=None):
"""
serialized_object should be a dict
"""
# Check settings switch for turning off XMPP alerts
if not settings.SEND_XMPP_ALERTS:
return
# Determine LVAlert node names
node_names = get_xmpp_node_names(event_or_superevent)
# Get uid
uid = event_or_superevent.graceid
# Create the output dictionary and serialize as JSON.
lva_data = {
'uid': uid,
'alert_type': alert_type,
'dispatched': f'{datetime.now(timezone.utc):%Y-%m-%d %H:%M:%S %Z}',
'data': serialized_object,
}
# Add serialized "parent" object
if serialized_parent is not None:
lva_data['object'] = serialized_parent
# Dump to JSON format:
# simplejson.dumps is needed to properly handle Decimal fields
msg = simplejson.dumps(lva_data)
# Try 'escaping' the message:
msg = escape(msg)
# Log message for debugging
logger.info("issue_xmpp_alerts: sending message {msg} for {uid}" \
.format(msg=msg, uid=uid))
payload = {
"topics": node_names,
"message": msg,
}
egad.send_alert("kafka", payload)
if settings.ENABLE_EGAD_KAFKA:
issue_xmpp_alerts = issue_xmpp_alerts_egad
else:
issue_xmpp_alerts = issue_xmpp_alerts_local
# See the VOEvent specification for details
# http://www.ivoa.net/Documents/latest/VOEvent.html
import datetime
import logging
import os
from scipy.constants import c, G, pi
import voeventparse as vp
from django.conf import settings
from django.urls import reverse
from core.time_utils import gpsToUtc
from core.urls import build_absolute_uri
from events.models import VOEventBase, Event
from events.models import CoincInspiralEvent, MultiBurstEvent, \
LalInferenceBurstEvent, MLyBurstEvent
from superevents.shortcuts import is_superevent
# Set up logger
logger = logging.getLogger(__name__)
###############################################################################
# SETUP #######################################################################
###############################################################################
# Dict of VOEvent type abbreviations and full strings
VOEVENT_TYPE_DICT = dict(VOEventBase.VOEVENT_TYPE_CHOICES)
# Used to create the Packet_Type parameter block
# Note: order matters. The order of this dict is the
# same as VOEVENT_TYPE_DICT.
PACKET_TYPES = {
VOEventBase.VOEVENT_TYPE_PRELIMINARY: (150, 'LVC_PRELIMINARY'),
VOEventBase.VOEVENT_TYPE_INITIAL: (151, 'LVC_INITIAL'),
VOEventBase.VOEVENT_TYPE_UPDATE: (152, 'LVC_UPDATE'),
VOEventBase.VOEVENT_TYPE_RETRACTION: (164, 'LVC_RETRACTION'),
VOEventBase.VOEVENT_TYPE_EARLYWARNING: (163, 'LVC_EARLY_WARNING'),
}
# Description strings
DEFAULT_DESCRIPTION = \
"Candidate gravitational wave event identified by low-latency analysis"
INSTRUMENT_DESCRIPTIONS = {
"H1": "H1: LIGO Hanford 4 km gravitational wave detector",
"L1": "L1: LIGO Livingston 4 km gravitational wave detector",
"V1": "V1: Virgo 3 km gravitational wave detector",
"K1": "K1: KAGRA 3 km gravitational wave detector"
}
###############################################################################
# MAIN ########################################################################
###############################################################################
def construct_voevent_file(obj, voevent, request=None):
# Setup ###################################################################
## Determine event or superevent
obj_is_superevent = False
if is_superevent(obj):
obj_is_superevent = True
event = obj.preferred_event
graceid = obj.default_superevent_id
obj_view_name = "superevents:view"
fits_view_name = "api:default:superevents:superevent-file-detail"
else:
event = obj
graceid = obj.graceid
obj_view_name = "view"
fits_view_name = "api:default:events:files"
# Get the event subclass (CoincInspiralEvent, MultiBurstEvent, etc.) and
# set that as the event
event = event.get_subclass_or_self()
## Let's convert that voevent_type to something nicer looking
voevent_type = VOEVENT_TYPE_DICT[voevent.voevent_type]
## Now build the IVORN.
if voevent_type == 'earlywarning':
type_string = 'EarlyWarning'
else:
type_string = voevent_type.capitalize()
voevent_id = '{gid}-{N}-{type_str}'.format(type_str=type_string,
gid=graceid, N=voevent.N)
## Determine role
if event.is_mdc() or event.is_test():
role = vp.definitions.roles.test
else:
role = vp.definitions.roles.observation
## Instantiate VOEvent
v = vp.Voevent(settings.VOEVENT_STREAM, voevent_id, role)
## Set root Description
if voevent_type != 'retraction':
v.Description = "Report of a candidate gravitational wave event"
# Overwrite the description for early warning events:
if voevent_type == 'earlywarning':
v.Description = "Early warning report of a candidate gravitational wave event"
# Who #####################################################################
## Remove Who.Description
v.Who.remove(v.Who.Description)
## Set Who.Date
vp.set_who(
v,
date=datetime.datetime.utcnow()
)
v.Who.Date += 'Z'
## Set Who.Author
vp.set_author(
v,
contactName="LIGO Scientific Collaboration, Virgo Collaboration, and KAGRA Collaboration"
)
# How #####################################################################
if voevent_type != 'retraction':
descriptions = [DEFAULT_DESCRIPTION]
# Add instrument descriptions
instruments = event.instruments.split(',')
for inst in INSTRUMENT_DESCRIPTIONS:
if inst in instruments:
descriptions.append(INSTRUMENT_DESCRIPTIONS[inst])
if voevent.coinc_comment:
descriptions.append("A gravitational wave trigger identified a "
"possible counterpart GRB")
vp.add_how(v, descriptions=descriptions)
# What ####################################################################
# UCD = Unified Content Descriptors
# http://monet.uni-sw.gwdg.de/twiki/bin/view/VOEvent/UnifiedContentDescriptors
# OR -- (from VOTable document, [21] below)
# http://www.ivoa.net/twiki/bin/view/IVOA/IvoaUCD
# http://cds.u-strasbg.fr/doc/UCD.htx
#
# which somehow gets you to:
# http://www.ivoa.net/Documents/REC/UCD/UCDlist-20070402.html
# where you might find some actual information.
# Unit / Section 4.3 of [21] which relies on [25]
# [21] http://www.ivoa.net/Documents/latest/VOT.html
# [25] http://vizier.u-strasbg.fr/doc/catstd-3.2.htx
#
# Basically, a string that makes sense to humans about what units a value
# is. eg. "m/s"
## Packet_Type param
p_packet_type = vp.Param(
"Packet_Type",
value=PACKET_TYPES[voevent.voevent_type][0],
ac=True
)
p_packet_type.Description = ("The Notice Type number is assigned/used "
"within GCN, eg type={typenum} is an {typedesc} notice").format(
typenum=PACKET_TYPES[voevent.voevent_type][0],
typedesc=PACKET_TYPES[voevent.voevent_type][1]
)
v.What.append(p_packet_type)
# Internal param
p_internal = vp.Param(
"internal",
value=int(voevent.internal),
ac=True
)
p_internal.Description = ("Indicates whether this event should be "
"distributed to LSC/Virgo/KAGRA members only")
v.What.append(p_internal)
## Packet serial number
p_serial_num = vp.Param(
"Pkt_Ser_Num",
value=voevent.N,
ac=True
)
p_serial_num.Description = ("A number that increments by 1 each time a "
"new revision is issued for this event")
v.What.append(p_serial_num)
## Event graceid or superevent ID
p_gid = vp.Param(
"GraceID",
value=graceid,
ucd="meta.id",
dataType="string"
)
p_gid.Description = "Identifier in GraceDB"
v.What.append(p_gid)
## Alert type parameter
if voevent_type == 'earlywarning':
voevent_at = 'EarlyWarning'
else:
voevent_at = voevent_type.capitalize()
p_alert_type = vp.Param(
"AlertType",
value = voevent_at,
ucd="meta.version",
dataType="string"
)
p_alert_type.Description = "VOEvent alert type"
v.What.append(p_alert_type)
## Whether the event is a hardware injection or not
p_hardware_inj = vp.Param(
"HardwareInj",
value=int(voevent.hardware_inj),
ucd="meta.number",
ac=True
)
p_hardware_inj.Description = ("Indicates that this event is a hardware "
"injection if 1, no if 0")
v.What.append(p_hardware_inj)
## Open alert parameter
p_open_alert = vp.Param(
"OpenAlert",
value=int(voevent.open_alert),
ucd="meta.number",
ac=True
)
p_open_alert.Description = ("Indicates that this event is an open alert "
"if 1, no if 0")
v.What.append(p_open_alert)
## Superevent page
p_detail_url = vp.Param(
"EventPage",
value=build_absolute_uri(
reverse(obj_view_name, args=[graceid]),
request
),
ucd="meta.ref.url",
dataType="string"
)
p_detail_url.Description = ("Web page for evolving status of this GW "
"candidate")
v.What.append(p_detail_url)
## Only for non-retractions
if voevent_type != 'retraction':
## Instruments
p_instruments = vp.Param(
"Instruments",
value=event.instruments,
ucd="meta.code",
dataType="string"
)
p_instruments.Description = ("List of instruments used in analysis to "
"identify this event")
v.What.append(p_instruments)
## False alarm rate
if event.far:
p_far = vp.Param(
"FAR",
value=float(max(event.far, settings.VOEVENT_FAR_FLOOR)),
ucd="arith.rate;stat.falsealarm",
unit="Hz",
ac=True
)
p_far.Description = ("False alarm rate for GW candidates with "
"this strength or greater")
v.What.append(p_far)
## Whether this is a significant candidate or not
p_significant = vp.Param(
"Significant",
value=int(voevent.significant),
ucd="meta.number",
ac=True
)
p_significant.Description = ("Indicates that this event is significant if "
"1, no if 0")
v.What.append(p_significant)
## Analysis group
## Special case: BURST-CWB-BBH search is a CBC group alert.
if (event.group.name == "Burst" and
(event.search and (event.pipeline.name == 'CWB' and
event.search.name == 'BBH'))
):
p_group = vp.Param(
"Group",
value="CBC",
ucd="meta.code",
dataType="string"
)
else:
p_group = vp.Param(
"Group",
value=event.group.name,
ucd="meta.code",
dataType="string"
)
p_group.Description = "Data analysis working group"
v.What.append(p_group)
## Analysis pipeline
p_pipeline = vp.Param(
"Pipeline",
value=event.pipeline.name,
ucd="meta.code",
dataType="string"
)
p_pipeline.Description = "Low-latency data analysis pipeline"
v.What.append(p_pipeline)
## Search type
if event.search:
p_search = vp.Param(
"Search",
value=event.search.name,
ucd="meta.code",
dataType="string"
)
p_search.Description = "Specific low-latency search"
v.What.append(p_search)
## RAVEN specific entries
if (is_superevent(obj) and voevent.raven_coinc):
ext_id = obj.em_type
ext_event = Event.getByGraceid(ext_id)
emcoinc_params = []
## External GCN ID
if ext_event.trigger_id:
p_extid = vp.Param(
"External_GCN_Notice_Id",
value=ext_event.trigger_id,
ucd="meta.id",
dataType="string"
)
p_extid.Description = ("GCN trigger ID of external event")
emcoinc_params.append(p_extid)
## External IVORN
if ext_event.ivorn:
p_extivorn = vp.Param(
"External_Ivorn",
value=ext_event.ivorn,
ucd="meta.id",
dataType="string"
)
p_extivorn.Description = ("IVORN of external event")
emcoinc_params.append(p_extivorn)
## External Pipeline
if ext_event.pipeline:
p_extpipeline = vp.Param(
"External_Observatory",
value=ext_event.pipeline.name,
ucd="meta.code",
dataType="string"
)
p_extpipeline.Description = ("External Observatory")
emcoinc_params.append(p_extpipeline)
## External Search
if ext_event.search:
p_extsearch = vp.Param(
"External_Search",
value=ext_event.search.name,
ucd="meta.code",
dataType="string"
)
p_extsearch.Description = ("External astrophysical search")
emcoinc_params.append(p_extsearch)
## Time Difference
if ext_event.gpstime and obj.t_0:
deltat = round(ext_event.gpstime - obj.t_0, 2)
p_deltat = vp.Param(
"Time_Difference",
value=float(deltat),
ucd="meta.code",
ac=True,
)
p_deltat.Description = ("Time difference between GW candidate "
"and external event, centered on the "
"GW candidate")
emcoinc_params.append(p_deltat)
## Temporal Coinc FAR
if obj.time_coinc_far:
p_coincfar = vp.Param(
"Time_Coincidence_FAR",
value=obj.time_coinc_far,
ucd="arith.rate;stat.falsealarm",
ac=True,
unit="Hz"
)
p_coincfar.Description = ("Estimated coincidence false alarm "
"rate in Hz using timing")
emcoinc_params.append(p_coincfar)
## Spatial-Temporal Coinc FAR
if obj.space_coinc_far:
p_coincfar_space = vp.Param(
"Time_Sky_Position_Coincidence_FAR",
value=obj.space_coinc_far,
ucd="arith.rate;stat.falsealarm",
ac=True,
unit="Hz"
)
p_coincfar_space.Description = ("Estimated coincidence false alarm "
"rate in Hz using timing and sky "
"position")
emcoinc_params.append(p_coincfar_space)
## RAVEN combined sky map
if voevent.combined_skymap_filename:
## Skymap group
### fits skymap URL
fits_skymap_url_comb = build_absolute_uri(
reverse(fits_view_name, args=[graceid,
voevent.combined_skymap_filename]),
request
)
p_fits_url_comb = vp.Param(
"joint_skymap_fits",
value=fits_skymap_url_comb,
ucd="meta.ref.url",
dataType="string"
)
p_fits_url_comb.Description = "Combined GW-External Sky Map FITS"
emcoinc_params.append(p_fits_url_comb)
## Create EMCOINC group
emcoinc_group = vp.Group(
emcoinc_params,
name='External Coincidence',
type='External Coincidence' # keep this only for backwards compatibility
)
emcoinc_group.Description = \
("Properties of joint coincidence found by RAVEN")
v.What.append(emcoinc_group)
# initial and update VOEvents must have a skymap.
# new feature (10/24/2016): preliminary VOEvents can have a skymap,
# but they don't have to.
if (voevent_type in ["initial", "update"] or
(voevent_type in ["preliminary", "earlywarning"] and voevent.skymap_filename != None)):
## Skymap group
### fits skymap URL
fits_skymap_url = build_absolute_uri(
reverse(fits_view_name, args=[graceid, voevent.skymap_filename]),
request
)
p_fits_url = vp.Param(
"skymap_fits",
value=fits_skymap_url,
ucd="meta.ref.url",
dataType="string"
)
p_fits_url.Description = "Sky Map FITS"
### Create skymap group with params
skymap_group = vp.Group(
[p_fits_url],
name="GW_SKYMAP",
type="GW_SKYMAP",
)
### Add to What
v.What.append(skymap_group)
## Analysis specific attributes
if voevent_type != 'retraction':
### Classification group (EM-Bright params; CBC only)
### In should also be present in case of cWB-BBH
em_bright_params = []
source_properties_params = []
if ( (isinstance(event, CoincInspiralEvent) or
(event.search and (event.pipeline.name == 'CWB' and
event.search.name == 'BBH'))
) and voevent_type != 'retraction'):
# EM-Bright mass classifier information for CBC event candidates
if voevent.prob_bns is not None:
p_pbns = vp.Param(
"BNS",
value=voevent.prob_bns,
ucd="stat.probability",
ac=True
)
p_pbns.Description = \
("Probability that the source is a binary neutron star "
"merger (both objects lighter than 3 solar masses)")
em_bright_params.append(p_pbns)
if voevent.prob_nsbh is not None:
p_pnsbh = vp.Param(
"NSBH",
value=voevent.prob_nsbh,
ucd="stat.probability",
ac=True
)
p_pnsbh.Description = \
("Probability that the source is a neutron star-black "
"merger (secondary lighter than 3 solar masses)")
em_bright_params.append(p_pnsbh)
if voevent.prob_bbh is not None:
p_pbbh = vp.Param(
"BBH",
value=voevent.prob_bbh,
ucd="stat.probability",
ac=True
)
p_pbbh.Description = ("Probability that the source is a "
"binary black hole merger (both objects "
"heavier than 3 solar masses)")
em_bright_params.append(p_pbbh)
#if voevent.prob_mass_gap is not None:
# p_pmassgap = vp.Param(
# "MassGap",
# value=voevent.prob_mass_gap,
# ucd="stat.probability",
# ac=True
# )
# p_pmassgap.Description = ("Probability that the source has at "
# "least one object between 3 and 5 "
# "solar masses")
# em_bright_params.append(p_pmassgap)
if voevent.prob_terrestrial is not None:
p_pterr = vp.Param(
"Terrestrial",
value=voevent.prob_terrestrial,
ucd="stat.probability",
ac=True
)
p_pterr.Description = ("Probability that the source is "
"terrestrial (i.e., a background noise "
"fluctuation or a glitch)")
em_bright_params.append(p_pterr)
# Add to source properties group
if voevent.prob_has_ns is not None:
p_phasns = vp.Param(
name="HasNS",
value=voevent.prob_has_ns,
ucd="stat.probability",
ac=True
)
p_phasns.Description = ("Probability that at least one object "
"in the binary has a mass that is "
"less than 3 solar masses")
source_properties_params.append(p_phasns)
if voevent.prob_has_remnant is not None:
p_phasremnant = vp.Param(
"HasRemnant",
value=voevent.prob_has_remnant,
ucd="stat.probability",
ac=True
)
p_phasremnant.Description = ("Probability that a nonzero mass "
"was ejected outside the central "
"remnant object")
source_properties_params.append(p_phasremnant)
if voevent.prob_has_mass_gap is not None:
p_pmassgap = vp.Param(
"HasMassGap",
value=voevent.prob_has_mass_gap,
ucd="stat.probability",
ac=True
)
p_pmassgap.Description = ("Probability that the source has at "
"least one object between 3 and 5 "
"solar masses")
source_properties_params.append(p_pmassgap)
if voevent.prob_has_ssm is not None:
p_phasssm = vp.Param(
"HasSSM",
value=voevent.prob_has_ssm,
ucd="stat.probability",
ac=True
)
p_phasssm.Description = ("Probability that the source has at "
"least one object less than 1 "
"solar mass")
source_properties_params.append(p_phasssm)
elif isinstance(event, MultiBurstEvent):
### Central frequency
p_central_freq = vp.Param(
"CentralFreq",
value=float(event.central_freq),
ucd="gw.frequency",
unit="Hz",
ac=True,
)
p_central_freq.Description = \
"Central frequency of GW burst signal"
v.What.append(p_central_freq)
### Duration
p_duration = vp.Param(
"Duration",
value=float(event.duration),
unit="s",
ucd="time.duration",
ac=True,
)
p_duration.Description = "Measured duration of GW burst signal"
v.What.append(p_duration)
elif isinstance(event, LalInferenceBurstEvent):
p_freq = vp.Param(
"CentralFreq",
value=float(event.frequency_mean),
ucd="gw.frequency",
unit="Hz",
ac=True,
)
p_freq.Description = "Central frequency of GW burst signal"
v.What.append(p_freq)
duration = event.quality_mean / (2 * pi * event.frequency_mean)
p_duration = vp.Param(
"Duration",
value=float(duration),
unit="s",
ucd="time.duration",
ac=True,
)
p_duration.Description = "Measured duration of GW burst signal"
v.What.append(p_duration)
elif isinstance(event, MLyBurstEvent):
p_central_freq = vp.Param(
"CentralFreq",
value=float(event.central_freq),
ucd="gw.frequency",
unit="Hz",
ac=True,
)
p_central_freq.Description = \
"Central frequency of GW burst signal"
v.What.append(p_central_freq)
p_duration = vp.Param(
"Duration",
value=float(event.duration),
unit="s",
ucd="time.duration",
ac=True,
)
p_duration.Description = "Measured duration of GW burst signal"
v.What.append(p_duration)
## Create classification group
classification_group = vp.Group(
em_bright_params,
name='Classification',
type='Classification' # keep this only for backwards compatibility
)
classification_group.Description = \
("Source classification: binary neutron star (BNS), neutron star-"
"black hole (NSBH), binary black hole (BBH), or "
"terrestrial (noise)")
v.What.append(classification_group)
## Create properties group
properties_group = vp.Group(
source_properties_params,
name='Properties',
type='Properties' # keep this only for backwards compatibility
)
properties_group.Description = \
("Qualitative properties of the source, conditioned on the "
"assumption that the signal is an astrophysical compact binary "
"merger")
v.What.append(properties_group)
# WhereWhen ###############################################################
# NOTE: we use a fake ra, dec, err, and units for creating the coords
# object. We are required to provide them by the voeventparse code, but
# our "format" for VOEvents didn't have a Position2D entry. So to make
# the code work but maintain the same format, we add fake information here,
# then remove it later.
coords = vp.Position2D(
ra=1, dec=2, err=3, units='degrees',
system=vp.definitions.sky_coord_system.utc_fk5_geo
)
observatory_id = 'LIGO Virgo'
vp.add_where_when(
v,
coords,
gpsToUtc(event.gpstime),
observatory_id
)
v.WhereWhen.ObsDataLocation.ObservationLocation.AstroCoords.Time.TimeInstant.ISOTime += 'Z'
# NOTE: now remove position 2D so the fake ra, dec, err, and units
# don't show up.
ol = v.WhereWhen.ObsDataLocation.ObservationLocation
ol.AstroCoords.remove(ol.AstroCoords.Position2D)
# Citations ###############################################################
if obj.voevent_set.count() > 1:
## Loop over previous VOEvents for this event or superevent and
## add them to citations
event_ivorns_list = []
for ve in obj.voevent_set.all():
# Oh, actually we need to exclude *this* voevent.
if ve.N == voevent.N:
continue
# Get cite type
if voevent_type == 'retraction':
cite_type = vp.definitions.cite_types.retraction
else:
cite_type = vp.definitions.cite_types.supersedes
# Set up event ivorn
ei = vp.EventIvorn(ve.ivorn, cite_type)
# Add event ivorn
event_ivorns_list.append(ei)
# Add citations
vp.add_citations(
v,
event_ivorns_list
)
# Get description for citation
desc = None
if voevent_type == 'preliminary':
desc = 'Initial localization is now available (preliminary)'
elif voevent_type == 'initial':
desc = 'Initial localization is now available'
elif voevent_type == 'update':
desc = 'Updated localization is now available'
elif voevent_type == 'retraction':
desc = 'Determined to not be a viable GW event candidate'
elif voevent_type == 'earlywarning':
desc = 'Early warning localization is now available'
if desc is not None:
v.Citations.Description = desc
# Return the document as a string, along with the IVORN ###################
xml = vp.dumps(v, pretty_print=True)
return xml, v.get('ivorn')
import base64
import logging
import OpenSSL.crypto
import OpenSSL.SSL
import re
from django.contrib.auth import get_user_model, authenticate
from django.conf import settings
from django.contrib.auth.models import User
from django.http import HttpResponseForbidden
from django.utils import timezone
from django.utils.http import unquote
from django.utils.translation import gettext_lazy as _
from django.urls import resolve
from rest_framework import authentication, exceptions
from ligoauth.models import X509Cert
from .utils import is_api_request
import scitokens
from jwt import InvalidTokenError
from scitokens.utils.errors import SciTokensException
from urllib.parse import unquote_plus
# Set up logger
logger = logging.getLogger(__name__)
class GraceDbBasicAuthentication(authentication.BasicAuthentication):
allow_ajax = False
api_only = True
def authenticate(self, request, *args, **kwargs):
"""
Same as base class, except we require the request to be directed
toward the basic auth API.
"""
# Make sure this request is directed to the API
if self.api_only and not is_api_request(request.path):
return None
# Don't allow this auth type for AJAX requests, since we don't want it
# to work for API requests made by the web views.
#if request.is_ajax() and not self.allow_ajax:
if request.headers.get('x-requested-with') == 'XMLHttpRequest' and not self.allow_ajax:
return None
# Call base class authenticate() method
return super(GraceDbBasicAuthentication, self).authenticate(request)
def authenticate_credentials(self, userid, password, request=None):
"""
Add a hacky password expiration check to the inherited method.
"""
user_auth_tuple = super(GraceDbBasicAuthentication, self) \
.authenticate_credentials(userid, password, request)
user = user_auth_tuple[0]
# Check password expiration
# NOTE: This is *super* hacky because we are using date_joined to store
# the date when the password was set. See managePassword() in
# userprofile.views.
password_expiry = user.date_joined + settings.PASSWORD_EXPIRATION_TIME
if timezone.now() > password_expiry:
msg = ('Your password has expired. Please log in to the web '
'interface and request another.')
raise exceptions.AuthenticationFailed(_(msg))
return user_auth_tuple
class GraceDbSciTokenAuthentication(authentication.BasicAuthentication):
class MultiIssuerEnforcer(scitokens.Enforcer):
def __init__(self, issuer, **kwargs):
if not isinstance(issuer, (tuple, list)):
issuer = [issuer]
super().__init__(issuer, **kwargs)
def _validate_iss(self, value):
return value in self._issuer
def authenticate(self, request, public_key=None):
# Get token from header
try:
bearer = request.headers["Authorization"]
except KeyError:
return None
auth_type, serialized_token = bearer.split()
if auth_type != "Bearer":
return None
# Deserialize token
try:
token = scitokens.SciToken.deserialize(
serialized_token,
# deserialize all tokens, enforce audience later
audience={"ANY"} | set(settings.SCITOKEN_AUDIENCE),
public_key=public_key,
)
except (InvalidTokenError, SciTokensException) as exc:
return None
# Enforce scitoken logic
enforcer = self.MultiIssuerEnforcer(
settings.SCITOKEN_ISSUER,
audience = settings.SCITOKEN_AUDIENCE,
)
try:
authz, path = settings.SCITOKEN_SCOPE.split(":", 1)
except ValueError:
authz = settings.SCITOKEN_SCOPE
path = None
if not enforcer.test(token, authz, path):
return None
# Get username from token 'Subject' claim.
try:
user = User.objects.get(username=token['sub'].lower())
except User.DoesNotExist:
try:
# Catch Kagra and robot accounts that don't have @ligo.org usernames
user = User.objects.get(username=token['sub'].split('@')[0].lower())
except User.DoesNotExist:
return None
if not user.is_active:
raise exceptions.AuthenticationFailed(
_('User inactive or deleted'))
return (user, None)
class GraceDbX509Authentication(authentication.BaseAuthentication):
"""
Authentication based on X509 certificate subject.
Certificate should be verified by Apache already.
"""
allow_ajax = False
api_only = True
www_authenticate_realm = 'api'
subject_dn_header = getattr(settings, 'X509_SUBJECT_DN_HEADER',
'SSL_CLIENT_S_DN')
issuer_dn_header = getattr(settings, 'X509_ISSUER_DN_HEADER',
'SSL_CLIENT_I_DN')
proxy_pattern = re.compile(r'^(.*?)(/CN=\d+)*$')
def authenticate(self, request):
# Make sure this request is directed to the API
if self.api_only and not is_api_request(request.path):
return None
# Don't allow this auth type for AJAX requests - this is because
# users with certificates in their browser can still authenticate via
# this mechanism in the web view (since it makes API queries), even
# when they are not logged in.
#if request.is_ajax() and not self.allow_ajax:
if request.headers.get('x-requested-with') == 'XMLHttpRequest' and not self.allow_ajax:
return None
# Try to get credentials from request headers.
user_cert_dn = self.get_cert_dn_from_request(request)
# If no user dn is found, pass on to the next auth method
if not user_cert_dn:
return None
return self.authenticate_credentials(user_cert_dn)
@classmethod
def authenticate_header(cls, request):
return 'X509 realm="{0}"'.format(cls.www_authenticate_realm)
@classmethod
def get_cert_dn_from_request(cls, request):
"""Get SSL headers and return DN for user"""
# Get subject and issuer DN from SSL headers
certdn = request.META.get(cls.subject_dn_header, None)
issuer = request.META.get(cls.issuer_dn_header, '')
# Handled proxied certificates
certdn = cls.extract_subject_from_proxied_cert(certdn, issuer)
return certdn
@classmethod
def extract_subject_from_proxied_cert(cls, subject, issuer):
"""
Handles the case of "impersonation proxies", where /CN=[0-9]+ is
appended to the end of the certificate subject. This occurs when you
generate a certificate and it "follows" you to another machine - you
effectively self-sign a copy of the certificate to use on the other
machine.
Example:
Albert generates a certificate with ligo-proxy-init on his laptop.
Subject and issuer when he pings the GraceDB server from his laptop:
/DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org
/DC=org/DC=cilogon/C=US/O=CILogon/CN=CILogon Basic CA 1
Subject and issuer when he gsisshs to an LDG cluster and then pings the
GraceDB server from there:
/DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org/CN=1492637212
/DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org
If he then gsisshs to *another* machine from there and repeats this,
he would get:
/DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org/CN=1492637212/CN=28732493
/DC=org/DC=cilogon/C=US/O=LIGO/CN=Albert Einstein albert.einstein@ligo.org/CN=1492637212
"""
if subject and issuer and subject.startswith(issuer):
# If we get here, we have an impersonation proxy, so we extract
# the proxy /CN=12345... part from the subject. Could also
# do it from the issuer (see above examples)
subject = cls.proxy_pattern.match(subject).group(1)
return subject
def authenticate_credentials(self, user_cert_dn):
certs = X509Cert.objects.filter(subject=user_cert_dn)
if not certs.exists():
raise exceptions.AuthenticationFailed(_('Invalid certificate '
'subject'))
cert = certs.first()
# Check if user is active
user = cert.user
if not user.is_active:
raise exceptions.AuthenticationFailed(
_('User inactive or deleted'))
return (user, None)
class GraceDbX509CertInfosAuthentication(GraceDbX509Authentication):
"""
Authentication based on X509 "infos" header.
Certificate should be verified by Traefik already.
"""
allow_ajax = False
api_only = True
infos_header = getattr(settings, 'X509_INFOS_HEADER',
'HTTP_X_FORWARDED_TLS_CLIENT_CERT_INFOS')
infos_pattern = re.compile(r'Subject="(.*?)".*Issuer="(.*?)"')
@classmethod
def get_cert_dn_from_request(cls, request):
"""Get SSL headers and return subject for user"""
# Get infos from request headers
infos = request.META.get(cls.infos_header, None)
# Unquote (handle pluses -> spaces)
infos_unquoted = unquote_plus(infos)
# Extract subject and issuer
subject, issuer = cls.infos_pattern.search(infos_unquoted).groups()
# Convert formats
subject = cls.convert_format(subject)
issuer = cls.convert_format(issuer)
# Handled proxied certificates
subject = cls.extract_subject_from_proxied_cert(subject, issuer)
return subject
@staticmethod
def convert_format(s):
# Convert subject or issuer strings from comma to slash format
s = s.replace(',', '/')
if not s.startswith('/'):
s = '/' + s
return s
class GraceDbX509FullCertAuthentication(GraceDbX509Authentication):
"""
Authentication based on a full X509 certificate. We verify the
certificate here.
"""
allow_ajax = False
api_only = True
www_authenticate_realm = 'api'
cert_header = getattr(settings, 'X509_CERT_HEADER',
'HTTP_X_FORWARDED_TLS_CLIENT_CERT')
def authenticate(self, request):
# Make sure this request is directed to the API
if self.api_only and not is_api_request(request.path):
return None
# Don't allow this auth type for AJAX requests - this is because
# users with certificates in their browser can still authenticate via
# this mechanism in the web view (since it makes API queries), even
# when they are not logged in.
#if request.is_ajax() and not self.allow_ajax:
if request.headers.get('x-requested-with') == 'XMLHttpRequest' and not self.allow_ajax:
return None
# Try to get certificate from request headers
cert_data = self.get_certificate_data_from_request(request)
# If no certificate is found, abort
if not cert_data:
return None
# Verify certificate
try:
certificate = self.verify_certificate_chain(cert_data)
except exceptions.AuthenticationFailed as e:
raise
except Exception as e:
raise exceptions.AuthenticationFailed(_('Certificate could not be '
'verified'))
return self.authenticate_credentials(certificate)
@classmethod
def get_certificate_data_from_request(cls, request):
"""Get certificate data from request"""
cert_quoted = request.META.get(cls.cert_header, None)
if cert_quoted is None:
return None
# Process the certificate a bit
cert_b64 = unquote(cert_quoted)
cert_der = base64.b64decode(cert_b64)
return cert_der
def verify_certificate_chain(self, cert_data, capath=settings.CAPATH):
# Load certificate data
certificate = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_ASN1, cert_data)
# Set up context and get certificate store
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
ctx.load_verify_locations(None, capath=capath)
store = ctx.get_cert_store()
# Verify certificate
store_ctx = OpenSSL.crypto.X509StoreContext(store, certificate)
store_ctx.verify_certificate()
# Check if expired
if certificate.has_expired():
raise exceptions.AuthenticationFailed(_('Certificate has expired'))
return certificate
def authenticate_credentials(self, certificate):
# Get subject and issuer
subject = self.get_certificate_subject_string(certificate)
issuer = self.get_certificate_issuer_string(certificate)
# Handled proxied certificates
subject = self.extract_subject_from_proxied_cert(subject, issuer)
# Authenticate credentials
return super(GraceDbX509FullCertAuthentication, self) \
.authenticate_credentials(subject)
@staticmethod
def get_certificate_subject_string(certificate):
subject = certificate.get_subject()
subject_decoded = [[word.decode("utf8") for word in sets]
for sets in subject.get_components()]
subject_string = '/' + "/".join(["=".join(c) for c in
subject_decoded])
return subject_string
@staticmethod
def get_certificate_issuer_string(certificate):
issuer = certificate.get_issuer()
issuer_decoded = [[word.decode("utf8") for word in sets]
for sets in issuer.get_components()]
issuer_string = '/' + "/".join(["=".join(c) for c in
issuer_decoded])
return issuer_string
class GraceDbAuthenticatedAuthentication(authentication.BaseAuthentication):
"""
If user is already authenticated by the main Django middleware,
don't make them authenticate again.
This is mostly (only?) used for access to the web-browsable API when
the user is already authenticated via Shibboleth.
"""
api_only = True
def authenticate(self, request):
# Make sure this request is directed to the API
if self.api_only and not is_api_request(request.path):
return None
if (hasattr(request, '_request') and hasattr(request._request, 'user')
and hasattr(request._request.user, 'is_authenticated') and
request._request.user.is_authenticated):
return (request._request.user, None)
else:
return None
import logging
from rest_framework.views import exception_handler
# Set up logger
logger = logging.getLogger(__name__)
def gracedb_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
if hasattr(exc, 'detail') and hasattr(exc.detail, 'values'):
# Combine values into one list
exc_out = [item for sublist in list(exc.detail.values())
for item in sublist]
# For only one exception, just print it rather than the list
if len(exc_out) == 1:
exc_out = exc_out[0]
# Update response data
response.data = exc_out
return response
import logging
from django.conf import settings
from rest_framework import permissions
# Set up logger
logger = logging.getLogger(__name__)
class IsPriorityUser(permissions.BasePermission):
"""Only allow users in the priority users group"""
message = 'You are not authorized to use this API.'
def has_permission(self, request, view):
return request.user.groups.filter(
name=settings.PRIORITY_USERS_GROUP).exists()