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 4552 additions and 93 deletions
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]))
......@@ -8,22 +8,20 @@ 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 search.query.labels import filter_for_labels
from superevents.shortcuts import is_superevent
from .lvalert import send_with_lvalert_overseer, send_with_lvalert_client
from .lvalert import send_with_lvalert_overseer, send_with_kafka_client
from . import egad
# Set up logger
logger = logging.getLogger(__name__)
if settings.USE_LVALERT_OVERSEER:
from hashlib import sha1
from multiprocessing import Manager
def get_xmpp_node_names(event_or_superevent):
"""
Utility function for determining the names of nodes to which XMPP
......@@ -50,7 +48,7 @@ def get_xmpp_node_names(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:
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)
......@@ -64,7 +62,7 @@ def get_xmpp_node_names(event_or_superevent):
return node_names
def issue_xmpp_alerts(event_or_superevent, alert_type, serialized_object,
def issue_xmpp_alerts_local(event_or_superevent, alert_type, serialized_object,
serialized_parent=None):
"""
serialized_object should be a dict
......@@ -74,6 +72,12 @@ def issue_xmpp_alerts(event_or_superevent, alert_type, serialized_object,
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)
......@@ -84,6 +88,7 @@ def issue_xmpp_alerts(event_or_superevent, alert_type, serialized_object,
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
......@@ -93,52 +98,104 @@ def issue_xmpp_alerts(event_or_superevent, alert_type, serialized_object,
# 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))
# Get manager ready for LVAlert Overseer (?)
if settings.USE_LVALERT_OVERSEER:
manager = Manager()
# Loop over LVAlert servers and nodes, issuing the alert to each
for overseer_instance in settings.LVALERT_OVERSEER_INSTANCES:
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).hexdigest()
message_id = sha1((node_name + msg).encode()).hexdigest()
# Log message
logger.info(("issue_xmpp_alerts: sending alert type {alert_type} "
"with message {msg_id} for {uid} to {node}").format(
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))
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, manager,
port)
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_xmpp_alerts: sending message with "
"LVAlert Overseer failed, trying lvalert_send"))
logger.critical(("issue_kafka_alerts: sending message with "
"Overseer failed, trying igwn-alert client code"))
# If not using LVAlert Overseer or if sending with overseer failed,
# use basic lvalert-client send
# 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)
server = lvalert_settings_dict.pop('lvalert_server')
send_with_lvalert_client(node_name, msg, server,
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_xmpp_alerts: error sending "
"message with lvalert client: {e}").format(e=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')
......@@ -6,10 +6,11 @@ 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, unquote_plus
from django.utils.translation import ugettext_lazy as _
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
......@@ -17,11 +18,17 @@ 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):
......@@ -33,6 +40,12 @@ class GraceDbBasicAuthentication(authentication.BasicAuthentication):
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)
......@@ -57,11 +70,76 @@ class GraceDbBasicAuthentication(authentication.BasicAuthentication):
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',
......@@ -75,6 +153,14 @@ class GraceDbX509Authentication(authentication.BaseAuthentication):
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)
......@@ -143,17 +229,8 @@ class GraceDbX509Authentication(authentication.BaseAuthentication):
'subject'))
cert = certs.first()
# Handle incorrect number of users for a certificate
num_users = cert.users.count()
if (num_users > 1):
raise exceptions.AuthenticationFailed(_('Multiple users have the '
'same certificate subject'))
elif (num_users == 0):
raise exceptions.AuthenticationFailed(_('No user found for this '
'certificate'))
user = cert.users.first()
# Check if user is active
user = cert.user
if not user.is_active:
raise exceptions.AuthenticationFailed(
_('User inactive or deleted'))
......@@ -166,6 +243,7 @@ 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')
......@@ -207,6 +285,7 @@ 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',
......@@ -218,6 +297,14 @@ class GraceDbX509FullCertAuthentication(GraceDbX509Authentication):
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)
......@@ -284,19 +371,22 @@ class GraceDbX509FullCertAuthentication(GraceDbX509Authentication):
@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.get_components()])
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.get_components()])
issuer_decoded])
return issuer_string
class GraceDbAuthenticatedAuthentication(authentication.BaseAuthentication):
"""
If user is already authenticated by the main Django middleware,
......
......@@ -13,7 +13,8 @@ def gracedb_exception_handler(exc, context):
if hasattr(exc, 'detail') and hasattr(exc.detail, 'values'):
# Combine values into one list
exc_out = [item for sublist in exc.detail.values() for item in sublist]
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:
......
from base64 import b64encode
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.urls import reverse
......@@ -31,9 +34,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can authenticate to API with correct password"""
# Set up and make request
url = api_reverse('api:root')
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
headers = {
'HTTP_AUTHORIZATION': 'Basic {0}'.format(user_and_pass),
}
......@@ -53,16 +59,20 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can't authenticate with wrong password"""
# Set up and make request
url = api_reverse('api:root')
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password='b4d')).decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password='b4d'
).encode()
).decode("ascii")
headers = {
'HTTP_AUTHORIZATION': 'Basic {0}'.format(user_and_pass),
}
response = self.client.get(url, data=None, **headers)
# Check response
self.assertEqual(response.status_code, 403)
self.assertIn('Invalid username/password', response.content)
self.assertContains(response, 'Invalid username/password',
status_code=403)
def test_user_authenticate_to_api_with_expired_password(self):
"""User can't authenticate with expired password"""
......@@ -73,17 +83,20 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
# Set up and make request
url = api_reverse('api:root')
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
headers = {
'HTTP_AUTHORIZATION': 'Basic {0}'.format(user_and_pass),
}
response = self.client.get(url, data=None, **headers)
# Check response
self.assertEqual(response.status_code, 403)
self.assertIn('Your password has expired', response.content)
self.assertContains(response, 'Your password has expired',
status_code=403)
class TestGraceDbX509Authentication(GraceDbApiTestBase):
......@@ -107,8 +120,8 @@ class TestGraceDbX509Authentication(GraceDbApiTestBase):
# Set up certificate for internal user account
cls.x509_subject = '/x509_subject'
cert = X509Cert.objects.create(subject=cls.x509_subject)
cert.users.add(cls.internal_user)
cert = X509Cert.objects.create(subject=cls.x509_subject,
user=cls.internal_user)
def test_user_authenticate_to_api_with_x509_cert(self):
"""User can authenticate to API with valid X509 certificate"""
......@@ -139,8 +152,8 @@ class TestGraceDbX509Authentication(GraceDbApiTestBase):
response = self.client.get(url, data=None, **headers)
# Check response
self.assertEqual(response.status_code, 401)
self.assertIn("Invalid certificate subject", response.content)
self.assertContains(response, 'Invalid certificate subject',
status_code=401)
def test_inactive_user_authenticate(self):
"""Inactive user can't authenticate"""
......@@ -156,8 +169,8 @@ class TestGraceDbX509Authentication(GraceDbApiTestBase):
response = self.client.get(url, data=None, **headers)
# Check response
self.assertEqual(response.status_code, 401)
self.assertIn("User inactive or deleted", response.content)
self.assertContains(response, 'User inactive or deleted',
status_code=401)
def test_authenticate_cert_with_proxy(self):
"""User can authenticate to API with proxied X509 certificate"""
......
......@@ -12,13 +12,22 @@ from user_sessions.middleware import SessionMiddleware
from api.backends import (
GraceDbBasicAuthentication, GraceDbX509Authentication,
GraceDbAuthenticatedAuthentication,
GraceDbSciTokenAuthentication, GraceDbAuthenticatedAuthentication,
)
from api.tests.utils import GraceDbApiTestBase
from api.utils import api_reverse
from ligoauth.middleware import ShibbolethWebAuthMiddleware
from ligoauth.models import X509Cert
import mock
import scitokens
import time
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key
from core.tests.utils import GraceDbTestBase
from django.test import override_settings
# Make sure to test password expiration
class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
......@@ -45,9 +54,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can authenticate to API with correct password"""
# Set up request
request = self.factory.get(api_reverse('api:root'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Authentication attempt
......@@ -60,8 +72,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can't authenticate with wrong password"""
# Set up request
request = self.factory.get(api_reverse('api:root'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password='b4d')).decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password='b4d'
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Authentication attempt should fail
......@@ -77,13 +93,16 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
# Set up request
request = self.factory.get(api_reverse('api:root'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Authentication attempt should fail
with self.assertRaisesRegexp(exceptions.AuthenticationFailed,
with self.assertRaisesRegex(exceptions.AuthenticationFailed,
'Your password has expired'):
user, other = self.backend_instance.authenticate(request)
......@@ -91,9 +110,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can't authenticate to a non-API URL path"""
# Set up request
request = self.factory.get(reverse('home'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Try to authenticate
......@@ -108,9 +130,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
# Set up request
request = self.factory.get(api_reverse('api:root'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Authentication attempt should fail
......@@ -118,6 +143,137 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
user, other = self.backend_instance.authenticate(request)
class TestGraceDbSciTokenAuthentication(GraceDbTestBase):
"""Test SciToken auth backend for API"""
TEST_ISSUER = ['local', 'local2']
TEST_AUDIENCE = ["TEST"]
TEST_SCOPE = "gracedb.read"
@classmethod
def setUpClass(cls):
super(TestGraceDbSciTokenAuthentication, cls).setUpClass()
# Attach request factory to class
cls.backend_instance = GraceDbSciTokenAuthentication()
cls.factory = APIRequestFactory()
@classmethod
def setUpTestData(cls):
super(TestGraceDbSciTokenAuthentication, cls).setUpTestData()
def setUp(self):
self._private_key = generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
self._public_key = self._private_key.public_key()
self._public_pem = self._public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
keycache = scitokens.utils.keycache.KeyCache.getinstance()
keycache.addkeyinfo("local", "sample_key", self._private_key.public_key())
now = int(time.time())
self._token = scitokens.SciToken(key = self._private_key, key_id="sample_key")
self._token.update_claims({
"iss": self.TEST_ISSUER,
"aud": self.TEST_AUDIENCE,
"scope": self.TEST_SCOPE,
"sub": str(self.internal_user),
})
self._serialized_token = self._token.serialize(issuer = "local")
self._no_kid_token = scitokens.SciToken(key = self._private_key)
@override_settings(
SCITOKEN_ISSUER="local",
SCITOKEN_AUDIENCE=["TEST"],
)
def test_user_authenticate_to_api_with_scitoken(self):
"""User can authenticate to API with valid Scitoken"""
# Set up request
request = self.factory.get(api_reverse('api:root'))
token_str = 'Bearer ' + self._serialized_token.decode()
request.headers = {'Authorization': token_str}
# Authentication attempt
user, other = self.backend_instance.authenticate(request, public_key=self._public_pem)
# Check authenticated user
self.assertEqual(user, self.internal_user)
@override_settings(
SCITOKEN_ISSUER="local",
SCITOKEN_AUDIENCE=["TEST"],
)
def test_user_authenticate_to_api_without_scitoken(self):
"""User can authenticate to API without valid Scitoken"""
# Set up request
request = self.factory.get(api_reverse('api:root'))
# Authentication attempt
resp = self.backend_instance.authenticate(request, public_key=self._public_pem)
# Check authentication response
assert resp == None
@override_settings(
SCITOKEN_ISSUER="local",
SCITOKEN_AUDIENCE=["TEST"],
)
def test_user_authenticate_to_api_with_wrong_audience(self):
"""User can authenticate to API with invalid Scitoken audience"""
# Set up request
request = self.factory.get(api_reverse('api:root'))
self._token["aud"] = "https://somethingelse.example.com"
serialized_token = self._token.serialize(issuer = "local")
token_str = 'Bearer ' + serialized_token.decode()
request.headers = {'Authorization': token_str}
# Authentication attempt
resp = self.backend_instance.authenticate(request, public_key=self._public_pem)
# Check authentication response
assert resp == None
@override_settings(
SCITOKEN_ISSUER="local",
SCITOKEN_AUDIENCE=["TEST"],
)
def test_user_authenticate_to_api_with_expired_scitoken(self):
"""User can authenticate to API with valid Scitoken"""
# Set up request
request = self.factory.get(api_reverse('api:root'))
serialized_token = self._token.serialize(issuer = "local", lifetime=-1)
token_str = 'Bearer ' + serialized_token.decode()
request.headers = {'Authorization': token_str}
# Authentication attempt
resp = self.backend_instance.authenticate(request, public_key=self._public_pem)
# Check authentication response
assert resp == None
@override_settings(
SCITOKEN_ISSUER="local",
SCITOKEN_AUDIENCE=["TEST"],
)
def test_inactive_user_authenticate_to_api_with_scitoken(self):
"""Inactive user can't authenticate with valid Scitoken"""
# Set internal user to inactive
self.internal_user.is_active = False
self.internal_user.save(update_fields=['is_active'])
# Set up request
request = self.factory.get(api_reverse('api:root'))
token_str = 'Bearer ' + self._serialized_token.decode()
request.headers = {'Authorization': token_str}
# Authentication attempt should fail
with self.assertRaises(exceptions.AuthenticationFailed):
user, other = self.backend_instance.authenticate(request, public_key=self._public_pem)
class TestGraceDbX509Authentication(GraceDbApiTestBase):
"""Test X509 certificate auth backend for API"""
......@@ -135,8 +291,8 @@ class TestGraceDbX509Authentication(GraceDbApiTestBase):
# Set up certificate for internal user account
cls.x509_subject = '/x509_subject'
cert = X509Cert.objects.create(subject=cls.x509_subject)
cert.users.add(cls.internal_user)
cert = X509Cert.objects.create(subject=cls.x509_subject,
user=cls.internal_user)
def test_user_authenticate_to_api_with_x509_cert(self):
"""User can authenticate to API with valid X509 certificate"""
......@@ -230,6 +386,7 @@ class TestGraceDbAuthenticatedAuthentication(GraceDbApiTestBase):
# Attach request factory to class
cls.backend_instance = GraceDbAuthenticatedAuthentication()
cls.factory = APIRequestFactory()
cls.get_response = mock.MagicMock()
def test_user_authenticate_to_api(self):
"""User can authenticate if already authenticated"""
......@@ -249,8 +406,8 @@ class TestGraceDbAuthenticatedAuthentication(GraceDbApiTestBase):
# as would be done in a view's initialize_request() method.
request = self.factory.get(api_reverse('api:root'))
# Preprocessing to set request.user to anonymous
SessionMiddleware().process_request(request)
AuthenticationMiddleware().process_request(request)
SessionMiddleware(self.get_response).process_request(request)
AuthenticationMiddleware(self.get_response).process_request(request)
request = Request(request=request)
# Try to authenticate user
......
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.core.cache import caches
......@@ -24,5 +27,5 @@ class TestThrottling(GraceDbApiTestBase):
# Second response should get throttled
response = self.request_as_user(url, "GET")
self.assertEqual(response.status_code, 429)
self.assertIn('Request was throttled', response.content)
self.assertContains(response, 'Request was throttled', status_code=429)
self.assertIn('Retry-After', response.headers)
from copy import deepcopy
import mock
try:
from functools import reduce
except ImportError: # python < 3
pass
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.core.cache import caches
......
from django.conf.urls import url, include
from django.urls import re_path, include
from .v1 import urls as v1_urls
from .v2 import urls as v2_urls
app_name = 'api'
urlpatterns = [
url(r'^', include('api.v1.urls', namespace='default')),
url(r'^v1/', include('api.v1.urls', namespace='v1')),
url(r'^v2/', include('api.v2.urls', namespace='v2')),
re_path(r'^', include((v1_urls, 'default'))),
re_path(r'^v1/', include((v1_urls, 'v1'))),
re_path(r'^v2/', include((v2_urls, 'v2'))),
]
......@@ -2,6 +2,7 @@ import logging
from django.urls import resolve, reverse as django_reverse
from rest_framework.settings import api_settings
from rest_framework.response import Response
from core.urls import build_absolute_uri
......@@ -71,6 +72,20 @@ def is_api_request(request_path):
api_app_name = 'api'
resolver_match = resolve(request_path)
if (resolver_match.app_name == api_app_name):
if (resolver_match.app_names and
resolver_match.app_names[0] == api_app_name):
return True
return False
class ResponseThenRun(Response):
"""
A response class that will do something after the response is sent
"""
def __init__(self, data, callback, callback_kwargs, **kwargs):
super(ResponseThenRun, self).__init__(data, **kwargs)
self.callback = callback
self.callback_kwargs = callback_kwargs
def close(self):
super().close()
self.callback(**self.callback_kwargs)
from __future__ import absolute_import
import logging
import six
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
......@@ -11,10 +14,47 @@ logger = logging.getLogger(__name__)
class EventGraceidField(GenericField, serializers.RelatedField):
default_error_messages = {
'invalid': _('Event graceid must be a string.'),
'bad_graceid': _('Not a valid graceid.'),
}
trim_whitespace = True
to_repr = 'graceid'
lookup_field = 'id'
lookup_field = 'pk'
model = Event
queryset = Event.objects.all()
def _validate_graceid(self, data):
# data should be a string at this point
prefix = data[0]
suffix = data[1:]
if not prefix in 'GEHMTD':
self.fail('bad_graceid')
try:
suffix = int(suffix)
except ValueError:
self.fail('bad_graceid')
def to_internal_value(self, data):
# Add string validation
if not isinstance(data, six.string_types):
self.fail('invalid')
value = six.text_type(data)
if self.trim_whitespace:
value = value.strip()
# Convert to uppercase (allows g1234 to work)
value = value.upper()
# Graceid validation
self._validate_graceid(value)
return super(EventGraceidField, self).to_internal_value(value)
def get_model_dict(self, data):
return {self.lookup_field: data[1:]}
def get_does_not_exist_error(self, graceid):
err_msg = "Event with graceid {graceid} does not exist.".format(
graceid=graceid)
return err_msg
from collections import OrderedDict
from django.utils.functional import cached_property
from django.utils.http import urlencode
from rest_framework import pagination
from rest_framework.response import Response
class CustomEventPagination(pagination.LimitOffsetPagination):
default_limit = 100
limit_query_param = 'count'
offset_query_param = 'start'
# Override the built-in counting method from here:
# https://github.com/encode/django-rest-framework/blob/3.4.7/rest_framework/pagination.py#L47
# And see if it can be cached. Like suggested here:
# https://stackoverflow.com/a/47357445
# update: caching works but I noticed in the browser that sometimes the numRows
# field retains its value from pervious queries. I don't know yet if it affects
# data fetching in the API, but i'm going to leave it commented for now.
# Another update: it would seem that fetching just the integer row-id for query
# results and then counting is faster than fetching the entire queryset. I didn't
# observe any change when doing large-ish counts on dev1 (~33000 events), but
# maybe postgres gets clever enough when doing larger queries. I'll leave it in
# and see what happens: https://stackoverflow.com/a/47357445
# @cached_property
@property
def _get_count(self):
"""
Determine an object count, supporting either querysets or regular lists.
"""
try:
return self.queryset.values('id').count()
except (AttributeError, TypeError):
return len(self.queryset)
def paginate_queryset(self, queryset, request, view=None):
self.limit = self.get_limit(request)
self.queryset = queryset
if self.limit is None:
return None
self.offset = self.get_offset(request)
self.count = self._get_count
self.request = request
if self.count > self.limit and self.template is not None:
self.display_page_controls = True
if self.count == 0 or self.offset > self.count:
return []
return list(self.queryset[self.offset:self.offset + self.limit])
def get_paginated_response(self, data):
numRows = self.count
# Get base URI
base_uri = self.request.build_absolute_uri(self.request.path)
# Construct custom link for "last" page
last = max(0, (numRows / self.limit)) * self.limit
param_dict = {
'start': last,
self.limit_query_param: self.limit,
}
last_uri = base_uri + '?' + urlencode(param_dict)
output = OrderedDict([
('numRows', numRows),
('events', data),
('links',
OrderedDict([
('self', self.request.build_absolute_uri()),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('first', base_uri),
('last', last_uri),
])),
])
return Response(output)