From 01a132effb2e5c79024cac063e9389634a745e7a Mon Sep 17 00:00:00 2001
From: Alexander Pace <alexander.pace@ligo.org>
Date: Wed, 6 Mar 2024 19:37:18 +0000
Subject: [PATCH] add icecube pipeline

---
 ..._external.json => event_external_grb.json} |   0
 .../source/dicts/event_external_neutrino.json |  48 +++
 docs/user_docs/source/dicts/event_ml.json     |  43 +++
 docs/user_docs/source/models.rst              |  23 +-
 gracedb/core/utils.py                         |  46 ++-
 .../migrations/0091_add_icecube_pipeline.py   |  52 +++
 .../events/migrations/0092_neutrinoevent.py   |  39 +++
 gracedb/events/models.py                      |  35 ++
 gracedb/events/translator.py                  |  52 +++
 gracedb/events/view_logic.py                  |   3 +
 gracedb/events/view_utils.py                  |  30 ++
 gracedb/events/views.py                       |   2 +
 .../0022_populate_icecube_uploaders.py        |  59 ++++
 .../templates/gracedb/event_detail_GRB.html   | 305 +++++++++---------
 .../templates/gracedb/event_detail_NE.html    | 165 ++++++++++
 15 files changed, 746 insertions(+), 156 deletions(-)
 rename docs/user_docs/source/dicts/{event_external.json => event_external_grb.json} (100%)
 create mode 100644 docs/user_docs/source/dicts/event_external_neutrino.json
 create mode 100644 docs/user_docs/source/dicts/event_ml.json
 create mode 100644 gracedb/events/migrations/0091_add_icecube_pipeline.py
 create mode 100644 gracedb/events/migrations/0092_neutrinoevent.py
 create mode 100644 gracedb/migrations/guardian/0022_populate_icecube_uploaders.py
 create mode 100644 gracedb/templates/gracedb/event_detail_NE.html

diff --git a/docs/user_docs/source/dicts/event_external.json b/docs/user_docs/source/dicts/event_external_grb.json
similarity index 100%
rename from docs/user_docs/source/dicts/event_external.json
rename to docs/user_docs/source/dicts/event_external_grb.json
diff --git a/docs/user_docs/source/dicts/event_external_neutrino.json b/docs/user_docs/source/dicts/event_external_neutrino.json
new file mode 100644
index 000000000..ee076c4bb
--- /dev/null
+++ b/docs/user_docs/source/dicts/event_external_neutrino.json
@@ -0,0 +1,48 @@
+{
+    "warnings": [],
+    "submitter": "wolfgang.pauli@ligo.org",
+    "created": "2024-03-01 20:18:46 UTC",
+    "group": "External",
+    "graceid": "E653136",
+    "pipeline": "IceCube",
+    "gpstime": 1384986914.64,
+    "reporting_latency": 8372630.079705,
+    "instruments": "",
+    "nevents": null,
+    "offline": false,
+    "search": "HEN",
+    "far": 4.667681380010147e-09,
+    "far_is_upper_limit": false,
+    "likelihood": null,
+    "labels": [],
+    "extra_attributes": {
+        "NeutrinoEvent": {
+            "ivorn": "ivo://nasa.gsfc.gcn/AMON#ICECUBE_GOLD_Event2023-11-25T22:34:56.64_24_138599_039138591_0",
+            "coord_system": "UTC-FK5-GEO",
+            "ra": 176.2601,
+            "dec": 52.6366,
+            "error_radius": 0.7792,
+            "far_ne": 0.1472,
+            "far_unit": "yr^-1",
+            "signalness": 0.6312,
+            "energy": 191.7344,
+            "src_error_90": 0.7792,
+            "src_error_50": 0.3035,
+            "amon_id": 13859939138591,
+            "run_id": 138599,
+            "event_id": 39138591,
+            "stream": 24
+        }
+    },
+    "superevent": null,
+    "superevent_neighbours": {},
+    "links": {
+        "neighbors": "https://gracedb-test.ligo.org/api/events/E653136/neighbors/",
+        "log": "https://gracedb-test.ligo.org/api/events/E653136/log/",
+        "emobservations": "https://gracedb-test.ligo.org/api/events/E653136/emobservation/",
+        "files": "https://gracedb-test.ligo.org/api/events/E653136/files/",
+        "labels": "https://gracedb-test.ligo.org/api/events/E653136/labels/",
+        "self": "https://gracedb-test.ligo.org/api/events/E653136",
+        "tags": "https://gracedb-test.ligo.org/api/events/E653136/tag/"
+    }
+}
diff --git a/docs/user_docs/source/dicts/event_ml.json b/docs/user_docs/source/dicts/event_ml.json
new file mode 100644
index 000000000..11e1e8ef6
--- /dev/null
+++ b/docs/user_docs/source/dicts/event_ml.json
@@ -0,0 +1,43 @@
+{
+    "submitter": "alan.turing@ligo.org",
+    "created": "2024-02-20 16:41:20 UTC",
+    "group": "Burst",
+    "graceid": "G648217",
+    "pipeline": "MLy",
+    "gpstime": 1392456472.379048,
+    "reporting_latency": 26026.564137,
+    "instruments": "H1,L1",
+    "nevents": null,
+    "offline": false,
+    "search": "AllSky",
+    "far": 5.855080848625316e-05,
+    "far_is_upper_limit": false,
+    "likelihood": null,
+    "labels": [],
+    "extra_attributes": {
+        "MLyBurst": {
+            "bandwidth": 64.0,
+            "central_freq": 309.246308659392,
+            "central_time": 1392456472.379048,
+            "duration": 0.1875,
+            "SNR": 6.015912207824155,
+            "detection_statistic": null,
+            "scores": {
+                "coherency": 0.0799756646156311,
+                "coincidence": 0.2124568223953247,
+                "combined": 0.016991375573191192
+            }
+        }
+    },
+    "superevent": null,
+    "superevent_neighbours": {},
+    "links": {
+        "neighbors": "https://gracedb-test.ligo.org/api/events/G648217/neighbors/",
+        "log": "https://gracedb-test.ligo.org/api/events/G648217/log/",
+        "emobservations": "https://gracedb-test.ligo.org/api/events/G648217/emobservation/",
+        "files": "https://gracedb-test.ligo.org/api/events/G648217/files/",
+        "labels": "https://gracedb-test.ligo.org/api/events/G648217/labels/",
+        "self": "https://gracedb-test.ligo.org/api/events/G648217",
+        "tags": "https://gracedb-test.ligo.org/api/events/G648217/tag/"
+    }
+}
diff --git a/docs/user_docs/source/models.rst b/docs/user_docs/source/models.rst
index 66c16e1a9..fcfe169e0 100644
--- a/docs/user_docs/source/models.rst
+++ b/docs/user_docs/source/models.rst
@@ -12,9 +12,9 @@ The different types of events in GraceDB are distinguished by the following para
 - ``Group``: the working group responsible for finding the candidate
     - values: ``CBC``, ``Burst``, ``Detchar``, ``External``, ``Test`` 
 - ``Pipeline``: the data analysis software tool used make the detection 
-    - values: ``MBTA``, ``MBTAOnline``, ``CWB``, ``CWB2G``, ``gstlal``, ``pycbc``, ``spiir``, ``HardwareInjection``, ``Fermi``, ``Swift``, ``INTEGRAL``, ``AGILE``, ``SNEWS``, ``oLIB``, ``MLy``
+    - values: ``CWB2G``, ``spiir``, ``HardwareInjection``, ``X``, ``Q``, ``Omega``, ``Ringdown``, ``Fermi``, ``Swift``, ``CWB``, ``SNEWS``, ``oLIB``, ``pycbc``, ``INTEGRAL``, ``AGILE``, ``gstlal``, ``MLy``, ``MBTAOnline``, ``MBTA``, ``CHIME``, ``PyGRB``, ``aframe``, ``SVOM``, ``IceCube``
 - ``Search``: the search activity which led to the detection 
-    - values: ``AllSky``, ``AllSkyLong``, ``LowMass``, ``HighMass``, ``GRB``, ``Supernova``, ``MDC``, ``BBH``, ``EarlyWarning``, ``IMBH``, ``SubGRB``, ``SubGRBTargeted``, ``VTInjection``
+    - values: ``AllSky``, ``LowMass``, ``HighMass``, ``GRB``, ``Supernova``, ``MDC``, ``LowMassSim``, ``AllSkyLong``, ``O2VirgoTest``, ``BBH``, ``IMBH``, ``SubGRB``, ``EarlyWarning``, ``SubGRBTargeted``, ``SSM``, ``FRB``, ``LensingSubthreshold``, ``VTInjection``, ``HEN``
 
 An individual "event stream" is specified by setting the values of these three parameters.
 For example, choosing ``Group=CBC``, ``Pipeline=gstlal``, and ``Search=LowMass`` selects the event stream consisting of low-mass inspiral events detected by the gstlal pipeline from the CBC group.
@@ -77,11 +77,24 @@ oLIB
 .. literalinclude:: dicts/event_olib.json
   :language: JSON
 
+Machine Learning (MLy, aframe)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-External
-~~~~~~~~
+.. literalinclude:: dicts/event_ml.json
+  :language: JSON
+
+
+External (GRB)
+~~~~~~~~~~~~~~
+
+.. literalinclude:: dicts/event_external_grb.json
+  :language: JSON
+
+
+External (Neutrino)
+~~~~~~~~~~~~~~~~~~~
 
-.. literalinclude:: dicts/event_external.json
+.. literalinclude:: dicts/event_external_neutrino.json
   :language: JSON
 
 
diff --git a/gracedb/core/utils.py b/gracedb/core/utils.py
index 22102af84..aa40550e6 100644
--- a/gracedb/core/utils.py
+++ b/gracedb/core/utils.py
@@ -16,8 +16,18 @@ ALPHABET = string.ascii_lowercase
 BASE = len(ALPHABET)
 ASCII_ALPHABET_START = ord('a') - 1
 
-# Unit conversions
-far_hr_to_yr = 86400*365.25
+# FAR Unit conversions (fixed 2024/3/1, big error in hr_to_yr)
+# Note that the converstion error that was just fixed was only
+# in the web page display, so there's thankfully no values in the
+# database to go back and fix.
+
+far_hr_to_yr = 24*365 	 # 1/yr = 1/hr * (8760 hr/year)
+far_yr_to_sec = 1.0/(60*60*24*365)    # 1/sec = 1/yr * (1 yr / 3.154e7 sec)
+far_hr_to_sec = far_hr_to_yr * far_yr_to_sec   # 1/sec =  1/hr * (8760 hr/ yr) * (1 yr/ 3.154e7 sec)
+
+# Unit formats:
+per_hr_formats = ['1/hr', 'hr^-1', '1/hour', 'hour^-1']
+per_yr_formats = ['1/yr', 'yr^-1', '1/year', 'year^-1']
 
 
 def int_to_letters(num, positive_only=True):
@@ -85,6 +95,38 @@ def display_far_hr_to_yr(display_far):
 
     return display_far_hr
 
+def return_far_in_hz(far_in, units=None):
+    """
+    A helper function that takes in a far value and a set of units
+    and returns the value in Hz. expects /hr or /yr atm.
+
+    Raising errors instead of 400's is probably safe here, since users
+    never directly call this function from the API.
+    """
+
+    # Check if units were provided, if not, quit
+    if not units: raise ValueError('no units input')
+
+    # Make sure the units are a string:
+    if not isinstance(units, str): raise TypeError('units must be a string')
+
+    # Change the units' formatting so they match the templates:
+    # make it lowercase and get rid of any spaces:
+    units_in = units.lower().replace(' ', '')
+
+    # Convert per hour:
+    if units_in in per_hr_formats:
+        return far_in * far_hr_to_sec
+
+    # Convert per year:
+    elif units_in in per_yr_formats:
+        return far_in * far_yr_to_sec
+
+    # Log and give up if not recognized:
+    else:
+        logger.debug('could not recognize input far units')
+        return None
+
 
 class CustomExceptionReporter(ExceptionReporter):
     """
diff --git a/gracedb/events/migrations/0091_add_icecube_pipeline.py b/gracedb/events/migrations/0091_add_icecube_pipeline.py
new file mode 100644
index 000000000..744770c2d
--- /dev/null
+++ b/gracedb/events/migrations/0091_add_icecube_pipeline.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+from django.db import migrations
+from events.models import Pipeline, Search
+
+# Creates initial search pipeline instances
+
+# List of search pipeline names
+NEW_PIPELINES = [
+    ('IceCube', Pipeline.PIPELINE_TYPE_EXTERNAL),
+]
+
+NEW_SEARCHES = [
+    'HEN',
+]
+
+def add_pipelines(apps, schema_editor):
+    Pipeline = apps.get_model('events', 'Pipeline')
+    Search = apps.get_model('events', 'Search')
+
+    # Create pipelines
+    for pipeline_name in NEW_PIPELINES:
+        pipeline, created = Pipeline.objects.get_or_create(name=pipeline_name[0])
+        pipeline.pipeline_type = pipeline_name[1]
+        pipeline.save()
+
+    # Create searches
+    for search_name in NEW_SEARCHES:
+        search, created = Search.objects.get_or_create(name=search_name)
+        search.save()
+
+def remove_pipelines(apps, schema_editor):
+    Pipeline = apps.get_model('events', 'Pipeline')
+    Search = apps.get_model('events', 'Search')
+
+    # Delete pipelines
+    for pipe in NEW_PIPELINES:
+        Pipeline.objects.filter(name=pipe[0]).delete()
+
+    # Delete searches
+    for search in NEW_SEARCHES:
+        Search.objects.get(name=search).delete()
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('events', '0090_add_svom_pipeline'),
+    ]
+
+    operations = [
+        migrations.RunPython(add_pipelines, remove_pipelines),
+    ]
diff --git a/gracedb/events/migrations/0092_neutrinoevent.py b/gracedb/events/migrations/0092_neutrinoevent.py
new file mode 100644
index 000000000..4b0cae390
--- /dev/null
+++ b/gracedb/events/migrations/0092_neutrinoevent.py
@@ -0,0 +1,39 @@
+# Generated by Django 4.2.10 on 2024-02-29 20:53
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('events', '0091_add_icecube_pipeline'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='NeutrinoEvent',
+            fields=[
+                ('event_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='events.event')),
+                ('ivorn', models.CharField(max_length=200, null=True)),
+                ('coord_system', models.CharField(max_length=200, null=True)),
+                ('ra', models.FloatField(null=True)),
+                ('dec', models.FloatField(null=True)),
+                ('error_radius', models.FloatField(null=True)),
+                ('signalness', models.FloatField(null=True)),
+                ('energy', models.FloatField(null=True)),
+                ('src_error_90', models.FloatField(null=True)),
+                ('src_error_50', models.FloatField(null=True)),
+                ('amon_id', models.BigIntegerField(null=True)),
+                ('run_id', models.PositiveIntegerField(null=True)),
+                ('event_id', models.PositiveIntegerField(null=True)),
+                ('stream', models.PositiveIntegerField(null=True)),
+                ('far_ne', models.FloatField(null=True)),
+                ('far_unit', models.CharField(max_length=10, null=True)),
+            ],
+            options={
+                'indexes': [models.Index(fields=['ivorn'], name='events_neut_ivorn_3ba7e5_idx'), models.Index(fields=['coord_system'], name='events_neut_coord_s_5392db_idx'), models.Index(fields=['ra'], name='events_neut_ra_5fd98d_idx'), models.Index(fields=['dec'], name='events_neut_dec_250378_idx'), models.Index(fields=['error_radius'], name='events_neut_error_r_b9e51f_idx'), models.Index(fields=['signalness'], name='events_neut_signaln_b90f17_idx'), models.Index(fields=['energy'], name='events_neut_energy_d11350_idx'), models.Index(fields=['src_error_90'], name='events_neut_src_err_27b466_idx'), models.Index(fields=['src_error_50'], name='events_neut_src_err_039571_idx'), models.Index(fields=['amon_id'], name='events_neut_amon_id_fbd455_idx'), models.Index(fields=['run_id'], name='events_neut_run_id_acd04a_idx'), models.Index(fields=['event_id'], name='events_neut_event_i_bb1d6a_idx'), models.Index(fields=['far_ne'], name='events_neut_far_ne_96b9e8_idx'), models.Index(fields=['stream'], name='events_neut_stream_37f8df_idx')],
+            },
+            bases=('events.event',),
+        ),
+    ]
diff --git a/gracedb/events/models.py b/gracedb/events/models.py
index 4908cdbaa..0998b4ed1 100644
--- a/gracedb/events/models.py
+++ b/gracedb/events/models.py
@@ -734,6 +734,41 @@ class GrbEvent(Event):
 #                  models.Index(fields=['redshift', ]),
 #                  models.Index(fields=['ivorn', ])]
 
+# External event subclass for neutrino observations. Created in 
+# support of IceCube integration. 
+class NeutrinoEvent(Event):
+    ivorn = models.CharField(max_length=200, null=True)
+    coord_system = models.CharField(max_length=200, null=True)
+    ra = models.FloatField(null=True)
+    dec = models.FloatField(null=True)
+    error_radius = models.FloatField(null=True)
+    signalness = models.FloatField(null=True)
+    energy = models.FloatField(null=True)
+    src_error_90 = models.FloatField(null=True)
+    src_error_50 = models.FloatField(null=True)
+    amon_id = models.BigIntegerField(null=True)
+    run_id = models.PositiveIntegerField(null=True)
+    event_id = models.PositiveIntegerField(null=True)
+    stream = models.PositiveIntegerField(null=True)
+    far_ne = models.FloatField(null=True) #neutrino event far
+    far_unit = models.CharField(max_length=10, null=True)
+
+    class Meta:
+        indexes = [models.Index(fields=['ivorn', ]),
+                   models.Index(fields=['coord_system', ]),
+                   models.Index(fields=['ra', ]),
+                   models.Index(fields=['dec', ]),
+                   models.Index(fields=['error_radius', ]),
+                   models.Index(fields=['signalness', ]),
+                   models.Index(fields=['energy', ]),
+                   models.Index(fields=['src_error_90', ]),
+                   models.Index(fields=['src_error_50', ]),
+                   models.Index(fields=['amon_id', ]),
+                   models.Index(fields=['run_id', ]),
+                   models.Index(fields=['event_id', ]),
+                   models.Index(fields=['far_ne', ]),
+                   models.Index(fields=['stream', ])]
+
 
 class CoincInspiralEvent(Event):
     ifos             = models.CharField(max_length=20, default="")
diff --git a/gracedb/events/translator.py b/gracedb/events/translator.py
index f4009c0c5..f6a98a0ed 100644
--- a/gracedb/events/translator.py
+++ b/gracedb/events/translator.py
@@ -13,6 +13,7 @@ from core.ligolw import GraceDBFlexibleContentHandler
 import voeventparse as vp
 
 from core.time_utils import utc_datetime_to_gps_float
+from core.utils import return_far_in_hz
 from core.vfile import create_versioned_file
 from .models import EventLog
 from .models import SingleInspiral
@@ -31,6 +32,13 @@ logger = logging.getLogger(__name__)
 # okay?
 use_in(GraceDBFlexibleContentHandler)
 
+# some attributes for NeutrinoEvents. These are case-sensitive
+# to how they are written in the VOEvent:
+
+neutrino_event_attrs = ['signalness', 'energy', 'src_error_90',
+                        'src_error_50', 'AMON_ID', 'run_id', 'event_id',
+                        'Stream']
+
 # A small helper function to unzip gzipped files. normally ligolw would
 # handle all this, but since we're selectively parsing certain tables,
 # we just unzip the initial upload and then write it as xml. NOTE: this 
@@ -351,6 +359,7 @@ def handle_uploaded_data(event, datafilename,
         # Get the event time from the VOEvent file
         error = None
         populateGrbEventFromVOEventFile(datafilename, event)
+
     elif pipeline == 'oLIB':
         # lambda function for converting to a type if not None
         typecast = lambda t, v: t(v) if v is not None else v
@@ -425,6 +434,8 @@ def handle_uploaded_data(event, datafilename,
 
         event.save()
 
+    elif pipeline in ['IceCube']:
+        populate_neutrinoevent_from_voevent(datafilename, event)
     else:
         # XXX should we do something here?
         pass
@@ -768,3 +779,44 @@ def populateGrbEventFromVOEventFile(filename, event):
 
     # Save event
     event.save()
+
+def populate_neutrinoevent_from_voevent(filename, event):
+    # Load file into vp.Voevent instance
+    with open(filename, 'rb') as f:
+        v = vp.load(f)
+
+    # Get gpstime:
+    utc_time = vp.convenience.get_event_time_as_utc(v)
+    gpstime = utc_datetime_to_gps_float(utc_time)
+
+    # Get position:
+    pos2d = vp.get_event_position(v)
+
+    # get top-level 'What' params:
+    voevent_what_params = vp.convenience.get_toplevel_params(v)
+
+    # Assign information to event
+    event.gpstime = gpstime
+
+    # NeutrinoEvent attributes:
+    event.ivorn = v.get('ivorn')
+    event.coord_system = pos2d.system
+    event.ra = pos2d.ra
+    event.dec = pos2d.dec
+    event.error_radius = pos2d.err
+
+    for param in neutrino_event_attrs:
+        if param in voevent_what_params:
+            setattr(event, param.lower(), voevent_what_params.get(param).get('value'))
+
+    # see if we captured FAR, and if so, parse the unit:
+    if 'FAR' in voevent_what_params:
+
+        event.far_ne = float(voevent_what_params.get('FAR').get('value'))
+        event.far_unit = voevent_what_params.get('FAR').get('unit')
+
+        # Now try and convert the far into hz for the base far:
+        event.far = return_far_in_hz(event.far_ne, event.far_unit)
+
+    # save the event:
+    event.save()
diff --git a/gracedb/events/view_logic.py b/gracedb/events/view_logic.py
index f1199469a..884c15afc 100644
--- a/gracedb/events/view_logic.py
+++ b/gracedb/events/view_logic.py
@@ -6,6 +6,7 @@ from .models import Pipeline, Search
 from .models import CoincInspiralEvent
 from .models import MultiBurstEvent
 from .models import MLyBurstEvent
+from .models import NeutrinoEvent
 from .models import GrbEvent
 from .models import SimInspiralEvent
 from .models import LalInferenceBurstEvent
@@ -67,6 +68,8 @@ def _createEventFromForm(request, form):
             event = LalInferenceBurstEvent()
         elif pipeline.name in ['MLy', 'aframe']:
             event = MLyBurstEvent()
+        elif pipeline.name in ['IceCube']:
+            event = NeutrinoEvent()
         else:
             event = Event()
 
diff --git a/gracedb/events/view_utils.py b/gracedb/events/view_utils.py
index 5e7ab58db..22523852f 100644
--- a/gracedb/events/view_utils.py
+++ b/gracedb/events/view_utils.py
@@ -221,6 +221,11 @@ def assemble_event_extra_attributes(event, request, is_alert):
                     event.mlyburstevent)
         except:
             pass
+        try:
+            extra_attributes_dict['NeutrinoEvent'] = neutrino_to_dict(
+                    event.neutrinoevent)
+        except:
+            pass
 
 
     # Finally add extra attributes for any SingleInspiral objects associated with this event
@@ -817,6 +822,31 @@ def lalinferenceburst_to_dict(event):
         pass
     return return_dict
 
+def neutrino_to_dict(event):
+    # A safe routine for returning a neutrinoevent dict
+    return_dict = {}
+    try:
+        return_dict.update({
+            "ivorn": event.ivorn,
+            "coord_system": event.coord_system,
+            "ra": event.ra,
+            "dec": event.dec,
+            "error_radius": event.error_radius,
+            "far_ne": event.far_ne,
+            "far_unit": event.far_unit,
+            "signalness": event.signalness,
+            "energy": event.energy,
+            "src_error_90": event.src_error_90,
+            "src_error_50": event.src_error_50,
+            "amon_id": event.amon_id,
+            "run_id": event.run_id,
+            "event_id": event.event_id,
+            "stream": event.stream,
+            })
+    except:
+        pass
+    return return_dict
+
 def signoffToDict(signoff):
     return {
         'submitter':    signoff.submitter.username,
diff --git a/gracedb/events/views.py b/gracedb/events/views.py
index f80bd58ac..4f1c54ee2 100644
--- a/gracedb/events/views.py
+++ b/gracedb/events/views.py
@@ -473,6 +473,8 @@ def view(request, event):
         templates.insert(0, 'gracedb/event_detail_oLIB.html')
     elif event.pipeline.name in ['MLy', 'aframe']:
         templates.insert(0, 'gracedb/event_detail_mly.html')
+    elif event.pipeline.name in ['IceCube']:
+        templates.insert(0, 'gracedb/event_detail_NE.html')
 
     return render(request, templates, context=context)
 
diff --git a/gracedb/migrations/guardian/0022_populate_icecube_uploaders.py b/gracedb/migrations/guardian/0022_populate_icecube_uploaders.py
new file mode 100644
index 000000000..6e6670f3a
--- /dev/null
+++ b/gracedb/migrations/guardian/0022_populate_icecube_uploaders.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# supports: https://git.ligo.org/computing/gracedb/server/-/issues/255
+
+from django.db import migrations
+
+# Creates UserObjectPermission objects which allow specific users
+# to add events for pipelines.  Based on current production database
+# content (27 October 2017)
+
+# List of pipeline names and lists of usernames who should
+# be allowed to add events for them
+PP_LIST = [
+    {
+        'pipeline': 'IceCube',
+        'usernames': [
+            'brandon.piotrzkowski@ligo.org',
+            'andrew.toivonen@ligo.org',
+            'emfollow',
+        ]
+    },
+]
+
+def add_permissions(apps, schema_editor):
+    User = apps.get_model('auth', 'User')
+    Permission = apps.get_model('auth', 'Permission')
+    UserObjectPermission = apps.get_model('guardian', 'UserObjectPermission')
+    Pipeline = apps.get_model('events', 'Pipeline')
+    ContentType = apps.get_model('contenttypes', 'ContentType')
+
+    perm = Permission.objects.get(codename='populate_pipeline')
+    ctype = ContentType.objects.get_for_model(Pipeline)
+    for pp_dict in PP_LIST:
+        pipeline, created = Pipeline.objects.get_or_create(name=pp_dict['pipeline'])
+
+        # Loop over users
+        for username in pp_dict['usernames']:
+
+            # Robot users should have been already created by ligoauth 0003,
+            # but we have to create human user accounts here
+            user, _ = User.objects.get_or_create(username=username)
+
+            # Create UserObjectPermission
+            uop, uop_created = UserObjectPermission.objects.get_or_create(
+                user=user, permission=perm, content_type=ctype,
+                object_pk=pipeline.id)
+
+def remove_permissions(apps, schema_editor):
+    pass
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('guardian', '0021_populate_svom_uploaders'),
+        ('events', '0091_add_icecube_pipeline'),
+    ]
+
+    operations = [
+        migrations.RunPython(add_permissions, remove_permissions),
+    ]
diff --git a/gracedb/templates/gracedb/event_detail_GRB.html b/gracedb/templates/gracedb/event_detail_GRB.html
index 4a7a5ed47..3614f8e4b 100644
--- a/gracedb/templates/gracedb/event_detail_GRB.html
+++ b/gracedb/templates/gracedb/event_detail_GRB.html
@@ -1,179 +1,186 @@
 {% extends "gracedb/event_detail.html" %}
 {% load timeutil %}
+{% load scientific %}
 
 {% block basic_info %}
 <div class="row my-3 justify-content-center">
-    <div class="col-md-10">
-        <div id="gdb-table-basic-grb">
-            <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
-                <thead>
-                    <tr>
-                        <th colspan="8"><h3>Basic Event Information</h3></th>
-                    </tr>
-                </thead>
-                <tbody>
+  <div class="col-md-10">
+    <div id="gdb-table-basic-grb">
+      <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
+	<thead>
+	  <tr>
+	    <th colspan="8"><h3>Basic Event Information</h3></th>
+	  </tr>
+	</thead>
+	<tbody>
 
-                    <tr>
-                        <td>UID</td>
-                        <td data-title="UID">{{ object.graceid }}</td>
-                    </tr>
-                    <tr>
-                        <td>Labels</td>
-                        <td data-title="Labels">
-                            {% if object.labelling_set.all %}
-                            {% for labelling in object.labelling_set.all %}
-                            <span style="color: {{labelling.label.defaultColor}};" class="label"
-                                                                                   data-toggle="tooltip"
-                                                                                   data-placement="top"
-                                                                                   data-html="true"
-                                                                                   title="<em>{{labelling.label.description}}</em><br>
-                                                                                   <b>Added by:</b> {{labelling.creator.get_full_name}}<br>
-                                                                                   <b>Added:</b> {{labelling.created}}">
-                                                                                   {{ labelling.label.name }}</span>
-                            {% endfor %}
-                            {% else %}
-                            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-                            {% endif %}
-                        </td>
-                    </tr>
-                    <tr>
-                        <td>Group</td>
-                        <td data-title="Group">{{ object.group.name }} </td>
-                    </tr>
-                    <tr>
-                        <td>Pipeline</td>
-                        <td data-title="Pipeline">{{ object.pipeline.name }} </td>
-                    </tr>
-                    <tr>
-                        <td>Search</td>
-                        <td data-title="Search">{{ object.search.name }} </td>
-                    </tr>
-                    <tr>
-                        <td>
-                            {{ "gps"|timeselect:"gps" }}
-                        </td>
-                        <td data-title="Event Time">{% if object.gpstime%}
-                            <!-- <span title="{{ object.gpstime|gpsdate }}">{{ object.gpstime }}</span> -->
-                            {{ object.gpstime|multiTime:"gps" }}
-                            {% endif %}</td>
-                    </tr>
-					<tr>
-						<td>Latency (s)</td>
-						<td>{{ object.reporting_latency|floatformat:"-3" }} </td>
-					</tr>
-                    <tr>
-                        <td>Links</td>
-                        <td data-title="Links"><a href="{{ object.weburl }}">Data</a></td>
-                    </tr>
-                    <tr>
-                        <td>
-                            {{ "submitted"|timeselect:"utc" }}
-                        </td>
-                        <td data-title="Submitted">{{ object.created|multiTime:"submitted" }}</td>
-                    </tr>
+	  <tr>
+	    <td>UID</td>
+	    <td data-title="UID">{{ object.graceid }}</td>
+	  </tr>
+	  <tr>
+	    <td>Labels</td>
+	    <td data-title="Labels">
+	      {% if object.labelling_set.all %}
+	      {% for labelling in object.labelling_set.all %}
+	      <span style="color: {{labelling.label.defaultColor}};" class="label"
+								     data-toggle="tooltip"
+								     data-placement="top"
+								     data-html="true"
+								     title="<em>{{labelling.label.description}}</em><br>
+								     <b>Added by:</b> {{labelling.creator.get_full_name}}<br>
+								     <b>Added:</b> {{labelling.created}}">
+								     {{ labelling.label.name }}</span>
+	      {% endfor %}
+	      {% else %}
+	      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+	      {% endif %}
+	    </td>
+	  </tr>
+	  <tr>
+	    <td>Group</td>
+	    <td data-title="Group">{{ object.group.name }} </td>
+	  </tr>
+	  <tr>
+	    <td>Pipeline</td>
+	    <td data-title="Pipeline">{{ object.pipeline.name }} </td>
+	  </tr>
+	  <tr>
+	    <td>Search</td>
+	    <td data-title="Search">{{ object.search.name }} </td>
+	  </tr>
+	  <tr>
+	    <td>
+	      {{ "gps"|timeselect:"gps" }}
+	    </td>
+	    <td data-title="Event Time">{% if object.gpstime%}
+	      <!-- <span title="{{ object.gpstime|gpsdate }}">{{ object.gpstime }}</span> -->
+	      {{ object.gpstime|multiTime:"gps" }}
+	      {% endif %}</td>
+	  </tr>
+	  <tr>
+	    <td>Latency (s)</td>
+	    <td>{{ object.reporting_latency|floatformat:"-3" }} </td>
+	  </tr>
+	  {% if object.far %}
+	  <tr>
+	    <td>FAR (Hz)</td>
+	    <td data-title="FAR">{{ object.far | scientific }} </td>
+	  </tr>
+	  {% endif %}
+	  <tr>
+	    <td>Links</td>
+	    <td data-title="Links"><a href="{{ object.weburl }}">Data</a></td>
+	  </tr>
+	  <tr>
+	    <td>
+	      {{ "submitted"|timeselect:"utc" }}
+	    </td>
+	    <td data-title="Submitted">{{ object.created|multiTime:"submitted" }}</td>
+	  </tr>
 
-    				{% if object.superevent %}
-					<tr>
-						<td>Superevent</td>
-						<td><a href="{{ object.superevent.get_web_url }}">{{ object.superevent.superevent_id }}</a></td>
-					</tr>
-                    {% endif %}
+	  {% if object.superevent %}
+	  <tr>
+	    <td>Superevent</td>
+	    <td><a href="{{ object.superevent.get_web_url }}">{{ object.superevent.superevent_id }}</a></td>
+	  </tr>
+	  {% endif %}
 
-                </tbody>
+	</tbody>
 
-            </table>
-        </div>
+      </table>
     </div>
+  </div>
 </div>
 {% endblock %}
 
 {% block analysis_specific %}
 {% if object.grbevent %}
 <div class="row my-3 justify-content-center">
-    <div class="col-md-10">
-        <div id="grb-info">
-            <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
-                <thead>
-                    <tr>
-                        <th colspan="2"><h3>GRB Info</h3></th>
-                    </tr>
-                </thead>
-                <tr><td>IVORN</td><td>{{object.ivorn}}</td></tr>
-                <tr><td>Author</td><td>{{object.author_shortname}}</td></tr>
-                <tr><td>Author IVORN</td><td>{{object.author_ivorn}}</td></tr>
-                <tr><td>Observatory</td><td>{{object.observatory_location_id}}</td></tr>
-                <tr><td>How</th><td>
-                        {% if object.how_reference_url %}
-                        <a href="{{object.how_reference_url}}" target="_new">{{object.how_description}}</a>
-                        {% else %}
-                        {{object.how_description}}
-                        {% endif %}
-                    </td></tr>
-                    {% if object.trigger_id %}
-                    <tr><td>Trigger ID</td><td>{{object.trigger_id}}</td></tr>
-                    {% endif %}
-                    {% if object.trigger_duration %}
-                    <tr><td>Trigger duration</td><td>{{object.trigger_duration}}</td></tr>
-                    {% endif %}
-                    {% if object.far %}
-                    <tr><td>FAR (Hz)</td><td>{{object.far}}</td></tr>
-                    {% endif %}
-                    {% if object.t90 %}
-                    <tr><td>T90</td><td>{{object.t90}}</td></tr>
-                    {% endif %}
-                    {% if object.redshift %}
-                    <tr><td>Redshift</td><td>{{object.redshift}}</td></tr>
-                    {% endif %}
-                    {% if object.designation %}
-                    <tr><td>Event designation</td><td>{{object.designation}}</td></tr>
-                    {% endif %}
-            </table>
-        </div>
+  <div class="col-md-10">
+    <div id="grb-info">
+      <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
+	<thead>
+	  <tr>
+	    <th colspan="2"><h3>GRB Info</h3></th>
+	  </tr>
+	</thead>
+	<tr><td>IVORN</td><td>{{object.ivorn}}</td></tr>
+	<tr><td>Author</td><td>{{object.author_shortname}}</td></tr>
+	<tr><td>Author IVORN</td><td>{{object.author_ivorn}}</td></tr>
+	<tr><td>Observatory</td><td>{{object.observatory_location_id}}</td></tr>
+	<tr><td>How</th><td>
+	    {% if object.how_reference_url %}
+	    <a href="{{object.how_reference_url}}" target="_new">{{object.how_description}}</a>
+	    {% else %}
+	    {{object.how_description}}
+	    {% endif %}
+	  </td></tr>
+	  {% if object.trigger_id %}
+	  <tr><td>Trigger ID</td><td>{{object.trigger_id}}</td></tr>
+	  {% endif %}
+	  {% if object.trigger_duration %}
+	  <tr><td>Trigger duration</td><td>{{object.trigger_duration}}</td></tr>
+	  {% endif %}
+	  {% if object.far %}
+	  <tr><td>FAR (Hz)</td><td>{{object.far}}</td></tr>
+	  {% endif %}
+	  {% if object.t90 %}
+	  <tr><td>T90</td><td>{{object.t90}}</td></tr>
+	  {% endif %}
+	  {% if object.redshift %}
+	  <tr><td>Redshift</td><td>{{object.redshift}}</td></tr>
+	  {% endif %}
+	  {% if object.designation %}
+	  <tr><td>Event designation</td><td>{{object.designation}}</td></tr>
+	  {% endif %}
+      </table>
     </div>
+  </div>
 </div>
 
 <div class="row my-3 justify-content-center">
-    <div class="col-md-10">
-        <div id="grb-info">
-            <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
-                <thead>
-                    <tr>
-                        <th colspan="2"><h3>GRB Location</h3></th>
-                    </tr>
-                </thead>
-                <tbody>
-                    <tr><td>Coord System</td><td>{{object.coord_system}}</td></tr>
-                    <tr><td>RA</td><td>{{object.ra}}</td></tr>
-                    <tr><td>Dec</td><td>{{object.dec}}</td></tr>
-                    <tr><td>Err</td><td>{{object.error_radius}}</td></tr>
-                </tbody>
-            </table> 
-        </div>
+  <div class="col-md-10">
+    <div id="grb-info">
+      <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
+	<thead>
+	  <tr>
+	    <th colspan="2"><h3>GRB Location</h3></th>
+	  </tr>
+	</thead>
+	<tbody>
+	  <tr><td>Coord System</td><td>{{object.coord_system}}</td></tr>
+	  <tr><td>RA</td><td>{{object.ra}}</td></tr>
+	  <tr><td>Dec</td><td>{{object.dec}}</td></tr>
+	  <tr><td>Err</td><td>{{object.error_radius}}</td></tr>
+	</tbody>
+      </table> 
     </div>
+  </div>
 </div>
 
 
 {% if can_update_grbevent %}
 <div class="row my-3 justify-content-center">
-    <div class="col-md-10">
-        <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
-            <thead>
-                <tr>
-                    <th colspan="2"><h3>Update GRB Event</h3></th>
-                </tr>
-            </thead>
-            <tbody>
+  <div class="col-md-10">
+    <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
+      <thead>
+	<tr>
+	  <th colspan="2"><h3>Update GRB Event</h3></th>
+	</tr>
+      </thead>
+      <tbody>
 
-                <form id="update_grbevent_form" action="{% url "legacy_apiweb:default:events:update-grbevent" object.graceid %}">
-                    {{ update_grbevent_form.as_table }}
-                    <tr>
-                        <td></td>
-                        <td><input type="submit" value="Update" disabled></td>
-                    </tr>
-                </form>
-            </tbody>
-        </table>
-    </div>
+	<form id="update_grbevent_form" action="{% url "legacy_apiweb:default:events:update-grbevent" object.graceid %}">
+	  {{ update_grbevent_form.as_table }}
+	  <tr>
+	    <td></td>
+	    <td><input type="submit" value="Update" disabled></td>
+	  </tr>
+	</form>
+      </tbody>
+    </table>
+  </div>
 </div>
 
 
diff --git a/gracedb/templates/gracedb/event_detail_NE.html b/gracedb/templates/gracedb/event_detail_NE.html
new file mode 100644
index 000000000..43f57cd0c
--- /dev/null
+++ b/gracedb/templates/gracedb/event_detail_NE.html
@@ -0,0 +1,165 @@
+{% extends "gracedb/event_detail.html" %}
+{% load timeutil %}
+{% load scientific %}
+
+{% block basic_info %}
+<div class="row my-3 justify-content-center">
+  <div class="col-md-10">
+    <div id="gdb-table-basic-ne">
+      <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
+	<thead>
+	  <tr>
+	    <th colspan="8"><h3>Basic Event Information</h3></th>
+	  </tr>
+	</thead>
+	<tbody>
+
+	  <tr>
+	    <td>UID</td>
+	    <td data-title="UID">{{ object.graceid }}</td>
+	  </tr>
+	  <tr>
+	    <td>Labels</td>
+	    <td data-title="Labels">
+	      {% if object.labelling_set.all %}
+	      {% for labelling in object.labelling_set.all %}
+	      <span style="color: {{labelling.label.defaultColor}};" class="label"
+								     data-toggle="tooltip"
+								     data-placement="top"
+								     data-html="true"
+								     title="<em>{{labelling.label.description}}</em><br>
+								     <b>Added by:</b> {{labelling.creator.get_full_name}}<br>
+								     <b>Added:</b> {{labelling.created}}">
+								     {{ labelling.label.name }}</span>
+	      {% endfor %}
+	      {% else %}
+	      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+	      {% endif %}
+	    </td>
+	  </tr>
+	  <tr>
+	    <td>Group</td>
+	    <td data-title="Group">{{ object.group.name }} </td>
+	  </tr>
+	  <tr>
+	    <td>Pipeline</td>
+	    <td data-title="Pipeline">{{ object.pipeline.name }} </td>
+	  </tr>
+	  <tr>
+	    <td>Search</td>
+	    <td data-title="Search">{{ object.search.name }} </td>
+	  </tr>
+	  <tr>
+	    <td>
+	      {{ "gps"|timeselect:"gps" }}
+	    </td>
+	    <td data-title="Event Time">{% if object.gpstime%}
+	      <!-- <span title="{{ object.gpstime|gpsdate }}">{{ object.gpstime }}</span> -->
+	      {{ object.gpstime|multiTime:"gps" }}
+	      {% endif %}</td>
+	  </tr>
+	  {% if object.far %}
+	  <tr>
+	    <td>FAR (Hz)</td>
+	    <td data-title="FAR">{{ object.far | scientific }} </td>
+	  </tr>
+	  {% endif %}
+	  <tr>
+	    <td>Latency (s)</td>
+	    <td>{{ object.reporting_latency|floatformat:"-3" }} </td>
+	  </tr>
+	  <tr>
+	    <td>Links</td>
+	    <td data-title="Links"><a href="{{ object.weburl }}">Data</a></td>
+	  </tr>
+	  <tr>
+	    <td>
+	      {{ "submitted"|timeselect:"utc" }}
+	    </td>
+	    <td data-title="Submitted">{{ object.created|multiTime:"submitted" }}</td>
+	  </tr>
+
+	  {% if object.superevent %}
+	  <tr>
+	    <td>Superevent</td>
+	    <td><a href="{{ object.superevent.get_web_url }}">{{ object.superevent.superevent_id }}</a></td>
+	  </tr>
+	  {% endif %}
+
+	</tbody>
+
+      </table>
+    </div>
+  </div>
+</div>
+{% endblock %}
+
+{% block analysis_specific %}
+{% if object.neutrinoevent %}
+<div class="row my-3 justify-content-center">
+  <div class="col-md-10">
+    <div id="ne-info">
+      <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
+	<thead>
+	  <tr>
+	    <th colspan="2"><h3>Neutrino Observation Info</h3></th>
+	  </tr>
+	</thead>
+	<tr><td>IVORN</td><td>{{object.ivorn}}</td></tr>
+	{% if object.amon_id %}
+	<tr><td>Alert Identification Number</td><td>{{object.amon_id}}</td></tr>
+	{% endif %}
+	{% if object.run_id %}
+	<tr><td>Run ID</td><td>{{object.run_id}}</td></tr>
+	{% endif %}
+	{% if object.event_id %}
+	<tr><td>Event ID</td><td>{{object.event_id}}</td></tr>
+	{% endif %}
+	{% if object.stream %}
+	<tr><td>IceCube Coincidence Stream ID</td><td>{{object.stream}}</td></tr>
+	{% endif %}
+	{% if object.far_ne %}
+	<tr><td>FAR ({{ object.far_unit }}) </td><td>{{object.far_ne}}</td></tr>
+	{% endif %}
+	{% if object.signalness %}
+	<tr><td>Signalness</td><td>{{object.signalness}}</td></tr>
+	{% endif %}
+	{% if object.energy %}
+	<tr><td>Energy</td><td>{{object.energy}}</td></tr>
+	{% endif %}
+	{% if object.src_err_90 %}
+	<tr><td>Source Angular Error (90%)</td><td>{{object.src_err_90}}</td></tr>
+	{% endif %}
+	{% if object.src_err_50 %}
+	<tr><td>Source Angular Error (50%)</td><td>{{object.src_err_50}}</td></tr>
+	{% endif %}
+      </table>
+    </div>
+  </div>
+</div>
+
+<div class="row my-3 justify-content-center">
+  <div class="col-md-10">
+    <div id="ne-loc">
+      <table class="table-hover table-condensed table-resp-gracedb shadow p-3 mb-5 rounded">
+	<thead>
+	  <tr>
+	    <th colspan="2"><h3>Neutrino Location</h3></th>
+	  </tr>
+	</thead>
+	<tbody>
+	  <tr><td>Coord System</td><td>{{object.coord_system}}</td></tr>
+	  <tr><td>RA</td><td>{{object.ra}}</td></tr>
+	  <tr><td>Dec</td><td>{{object.dec}}</td></tr>
+	  <tr><td>Err</td><td>{{object.error_radius}}</td></tr>
+	</tbody>
+      </table> 
+    </div>
+  </div>
+</div>
+
+
+{% endif %}
+
+{% endblock %}
+
-- 
GitLab