From 1135690d05819986f0b0223e2be26312c05b9bb6 Mon Sep 17 00:00:00 2001
From: Branson Stephens <branson.stephens@ligo.org>
Date: Fri, 11 Sep 2015 13:55:39 -0500
Subject: [PATCH] Advocate signoff feature. Integrated with operator signoff.

---
 gracedb/api.py                                |  18 +-
 gracedb/forms.py                              |   6 +-
 .../migrations/0007_auto_new_signoff_model.py |  47 +++
 .../migrations/0008_add_advocate_labels.py    |  19 +
 gracedb/models.py                             |   6 +-
 gracedb/query.py                              |   2 +-
 gracedb/urls.py                               |   2 +-
 gracedb/urls_rest.py                          |   2 +-
 gracedb/view_utils.py                         |  11 +-
 gracedb/views.py                              | 328 ++++++------------
 migrations/auth/0008_add_advocates.py         |  33 ++
 settings/default.py                           |   2 +
 templates/gracedb/event_detail.html           |  63 +++-
 templates/gracedb/event_detail_script.js      |  11 +-
 14 files changed, 307 insertions(+), 243 deletions(-)
 create mode 100644 gracedb/migrations/0007_auto_new_signoff_model.py
 create mode 100644 gracedb/migrations/0008_add_advocate_labels.py
 create mode 100644 migrations/auth/0008_add_advocates.py

diff --git a/gracedb/api.py b/gracedb/api.py
index b3f820c55..caab25189 100644
--- a/gracedb/api.py
+++ b/gracedb/api.py
@@ -24,7 +24,7 @@ from view_utils import fix_old_creation_request
 from view_utils import eventToDict, eventLogToDict, labelToDict
 from view_utils import embbEventLogToDict, voeventToDict
 from view_utils import emObservationToDict, skymapViewerEMObservationToDict
-from view_utils import operatorSignoffToDict
+from view_utils import signoffToDict
 from view_utils import reverse
 
 from translator import handle_uploaded_data
@@ -1466,8 +1466,8 @@ class GracedbRoot(APIView):
         tag = tag.replace("0", "{n}")
         tag = tag.replace("tagname", "{tagname}")
 
-        operatorsignofflist = reverse("operatorsignoff-list", args=["G1200"], request=request)
-        operatorsignofflist = operatorsignofflist.replace("G1200", "{graceid}")
+        signofflist = reverse("signoff-list", args=["G1200"], request=request)
+        signofflist = signofflist.replace("G1200", "{graceid}")
 
         # XXX Need a template for the tag list?
 
@@ -1482,7 +1482,7 @@ class GracedbRoot(APIView):
                 "filemeta-template" : filemeta,
                 "tag-template" : tag,
                 "taglist-template" : taglist,
-                "operatorsignoff-list-template": operatorsignofflist,
+                "signoff-list-template": signofflist,
                 }
 
         return Response({
@@ -1854,11 +1854,9 @@ class OperatorSignoffList(APIView):
 
     @event_and_auth_required
     def get(self, request, event):
-        operator_signoff_set = event.operatorsignoff_set.all()
-        count = operator_signoff_set.count()
-
-        operator_signoff = [ operatorSignoffToDict(os)
-                for os in operator_signoff_set.iterator() ]
+        signoff_set = event.signoff_set.all()
+        count = signoff_set.count()
+        signoff = [ signoffToDict(s) for s in signoff_set.iterator() ]
 
         rv = {
                 'start': 0,
@@ -1868,7 +1866,7 @@ class OperatorSignoffList(APIView):
                     'first' : request.build_absolute_uri(),
                     'last' : request.build_absolute_uri(),
                     },
-                'operator_signoff' : operator_signoff,
+                'signoff' : signoff,
              }
         return Response(rv)
 
diff --git a/gracedb/forms.py b/gracedb/forms.py
index 6cbe08de5..1dae6b981 100644
--- a/gracedb/forms.py
+++ b/gracedb/forms.py
@@ -3,7 +3,7 @@ from django import forms
 from django.utils.safestring import mark_safe
 from django.utils.html import escape
 from models import Event, Group, Label
-from models import Pipeline, Search, OperatorSignoff
+from models import Pipeline, Search, Signoff
 from django.contrib.auth.models import User
 from django.core.exceptions import FieldError
 from django.forms import ModelForm
@@ -81,7 +81,7 @@ class EventSearchForm(forms.Form):
     labels = forms.MultipleChoiceField(choices=labelChoices, required=False)
     get_neighbors = forms.BooleanField(required=False)
 
-class OperatorSignoffForm(ModelForm):
+class SignoffForm(ModelForm):
     class Meta:
-        model = OperatorSignoff
+        model = Signoff
         fields = [ 'status', 'comment' ] 
diff --git a/gracedb/migrations/0007_auto_new_signoff_model.py b/gracedb/migrations/0007_auto_new_signoff_model.py
new file mode 100644
index 000000000..e59715275
--- /dev/null
+++ b/gracedb/migrations/0007_auto_new_signoff_model.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('gracedb', '0006_add_operator_signoff_labels'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Signoff',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('signoff_type', models.CharField(max_length=3, choices=[(b'OP', b'operator'), (b'ADV', b'advocate')])),
+                ('instrument', models.CharField(blank=True, max_length=2, choices=[(b'H1', b'LHO'), (b'L1', b'LLO'), (b'V1', b'Virgo')])),
+                ('status', models.CharField(max_length=2, choices=[(b'OK', b'OKAY'), (b'NO', b'NOT OKAY')])),
+                ('comment', models.TextField(blank=True)),
+                ('event', models.ForeignKey(to='gracedb.Event')),
+                ('submitter', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.AlterUniqueTogether(
+            name='operatorsignoff',
+            unique_together=set([]),
+        ),
+        migrations.RemoveField(
+            model_name='operatorsignoff',
+            name='event',
+        ),
+        migrations.RemoveField(
+            model_name='operatorsignoff',
+            name='submitter',
+        ),
+        migrations.DeleteModel(
+            name='OperatorSignoff',
+        ),
+        migrations.AlterUniqueTogether(
+            name='signoff',
+            unique_together=set([('event', 'instrument')]),
+        ),
+    ]
diff --git a/gracedb/migrations/0008_add_advocate_labels.py b/gracedb/migrations/0008_add_advocate_labels.py
new file mode 100644
index 000000000..5546b1931
--- /dev/null
+++ b/gracedb/migrations/0008_add_advocate_labels.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+def add_advocate_labels(apps, schema_editor):
+    Label = apps.get_model("gracedb", "Label")
+    for name in ['ADVREQ', 'ADVOK', 'ADVNO']:
+        Label.objects.create(name=name)
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('gracedb', '0007_auto_new_signoff_model'),
+    ]
+
+    operations = [
+        migrations.RunPython(add_advocate_labels),
+    ]
diff --git a/gracedb/models.py b/gracedb/models.py
index e9b47a6fb..21ee7987a 100644
--- a/gracedb/models.py
+++ b/gracedb/models.py
@@ -1098,12 +1098,14 @@ class VOEvent(models.Model):
 
 INSTRUMENTS = ( ('H1', 'LHO'), ('L1', 'LLO'), ('V1', 'Virgo') )
 OPERATOR_STATUSES = ( ('OK', 'OKAY'), ('NO', 'NOT OKAY') )
-class OperatorSignoff(models.Model):
+SIGNOFF_TYPE_CHOICES = ( ('OP', 'operator'), ('ADV', 'advocate') )
+class Signoff(models.Model):
     class Meta:
         unique_together = ("event","instrument")
     submitter = models.ForeignKey(DjangoUser)
     event = models.ForeignKey(Event)
-    instrument = models.CharField(max_length=2, choices=INSTRUMENTS)
+    signoff_type = models.CharField(max_length=3, choices=SIGNOFF_TYPE_CHOICES, blank=False)
+    instrument = models.CharField(max_length=2, choices=INSTRUMENTS, blank=True)
     status = models.CharField(max_length=2, choices=OPERATOR_STATUSES, blank=False)
     comment = models.TextField(blank=True)
 
diff --git a/gracedb/query.py b/gracedb/query.py
index 3015d13c4..66a433ef9 100644
--- a/gracedb/query.py
+++ b/gracedb/query.py
@@ -181,7 +181,7 @@ createdQ = createdQ.setParseAction(maybeRange("created"))
 
 # Labels
 # XXX should we not get these from the DB?
-labelNames = ["DQV", "INJ", "LUMIN_NO", "LUMIN_GO", "SWIFT_NO", "SWIFT_GO", "EM_READY", "cWB_r","cWB_s", "H1OPS", "L1OPS", "V1OPS"]
+labelNames = ["DQV", "INJ", "LUMIN_NO", "LUMIN_GO", "SWIFT_NO", "SWIFT_GO", "EM_READY", "cWB_r","cWB_s", "H1OPS", "L1OPS", "V1OPS", "H1OK", "H1NO", "L1OK", "L1NO", "V1OK", "V1NO", "ADVREQ", "ADVOK", "ADVNO"]
 label = Or([CaselessLiteral(n) for n in labelNames]).\
         setParseAction( lambda toks: Q(labels__name=toks[0]) )
 
diff --git a/gracedb/urls.py b/gracedb/urls.py
index 61a233b94..2c71cc045 100644
--- a/gracedb/urls.py
+++ b/gracedb/urls.py
@@ -19,7 +19,7 @@ urlpatterns = patterns('gracedb.views',
     url (r'^(?P<graceid>[GEHMT]\d+)$', 'view', name="view2"),
     url (r'^(?P<graceid>[GEHMT]\d+)/t90/$', 'modify_t90', name="modify_t90"),
     url (r'^(?P<graceid>[GEHMT]\d+)/perms/$', 'modify_permissions', name="modify_permissions"),
-    url (r'^(?P<graceid>[GEHMT]\d+)/signoff/$', 'modify_operator_signoff', name="modify_operator_signoff"),
+    url (r'^(?P<graceid>[GEHMT]\d+)/signoff/$', 'modify_signoff', name="modify_signoff"),
     url (r'^(?P<graceid>[GEHMT]\d+)/files/$', 'file_list', name="file_list"),
     url (r'^(?P<graceid>[GEHMT]\d+)/files/(?P<filename>.*)$', download, name="file"),
     url (r'^(?P<graceid>[GEHMT]\d+)/log/(?P<num>([\d]*|preview))$', 'logentry', name="logentry"),
diff --git a/gracedb/urls_rest.py b/gracedb/urls_rest.py
index 048550373..6236d8317 100644
--- a/gracedb/urls_rest.py
+++ b/gracedb/urls_rest.py
@@ -107,7 +107,7 @@ urlpatterns = patterns('gracedb.api',
 
     # Operator Signoff Resources
     url (r'events/(?P<graceid>[GEHMT]\d+)/signoff/$',
-        OperatorSignoffList.as_view(), name='operatorsignoff-list'),
+        OperatorSignoffList.as_view(), name='signoff-list'),
 
 
     # Performance stats
diff --git a/gracedb/view_utils.py b/gracedb/view_utils.py
index 70b3d5167..8c62738b0 100644
--- a/gracedb/view_utils.py
+++ b/gracedb/view_utils.py
@@ -526,12 +526,13 @@ def singleInspiralToDict(single_inspiral):
             rv.update({ field_name: value })
     return rv
 
-def operatorSignoffToDict(operator_signoff):
+def signoffToDict(signoff):
     return {
-        'submitter':  operator_signoff.submitter.username,
-        'instrument': operator_signoff.instrument,
-        'status':     operator_signoff.status,
-        'comment':    operator_signoff.comment,
+        'submitter':    signoff.submitter.username,
+        'instrument':   signoff.instrument,
+        'status':       signoff.status,
+        'comment':      signoff.comment,
+        'signoff_type': signoff.signoff_type
     } 
 
 #---------------------------------------------------------------------------------------
diff --git a/gracedb/views.py b/gracedb/views.py
index c571bff6f..21e40bbd0 100644
--- a/gracedb/views.py
+++ b/gracedb/views.py
@@ -7,8 +7,8 @@ from django.core.urlresolvers import reverse
 from django.shortcuts import render_to_response
 
 from models import Event, Group, EventLog, Label, Tag, Pipeline, Search, GrbEvent
-from models import EMGroup, OperatorSignoff
-from forms import CreateEventForm, EventSearchForm, SimpleSearchForm, OperatorSignoffForm
+from models import EMGroup, Signoff
+from forms import CreateEventForm, EventSearchForm, SimpleSearchForm, SignoffForm
 
 from django.contrib.auth.models import User, Permission
 from django.contrib.auth.models import Group as AuthGroup
@@ -324,29 +324,51 @@ def view(request, event):
     if event.pipeline.name in settings.GRB_PIPELINES:
         context['can_modify_t90'] = request.user.has_perm('gracedb.t90_grbevent')
 
-    # Does the user have permission to sign off on the event?
-    signoff_authorized = False
+    # Does the user have permission to sign off on the event as the control room operator?
+    operator_signoff_authorized = False
     # XXX Note that this may not be the best way to perform the authorization check.
     # In particular, this assumes that the user can only be a member of one group 
     # at a time. That should be the case, however, as the control room machines are 
     # physically well separated and should have different IPs.
     for group in request.user.groups.all():
         if '_control_room' in group.name:
-            signoff_authorized = True
+            operator_signoff_authorized = True
             context['signoff_instrument'] = group.name[:2].upper()
-            context['signoff_form'] = OperatorSignoffForm()
+            context['signoff_form'] = SignoffForm()
             instrument = group.name[:2].upper()
             try:
-                context['signoff_object'] = OperatorSignoff.objects.get(event=event, instrument=instrument)
+                context['operator_signoff_object'] = Signoff.objects.get(event=event, 
+                    instrument=instrument, signoff_type='OP')
             except:
-                context['signoff_object'] = None
-            label_name = instrument + 'OPS'
-            label_exists = label_name in [l.label.name for l in event.labelling_set.all()]
+                context['operator_signoff_object'] = None
+            req_label = instrument + 'OPS'
+            label_exists = req_label in [l.label.name for l in event.labelling_set.all()]
 
-            context['signoff_active'] = label_exists or context['signoff_object']
+            context['operator_signoff_active'] = label_exists or context['operator_signoff_object']
 
             break
-    context['signoff_authorized'] = signoff_authorized
+    context['operator_signoff_authorized'] = operator_signoff_authorized
+
+    # XXX A lot of repetition here. Hopefully this will be fixed later.
+    # Does the user have permission to sign off on the event as an EM advocate?
+    advocate_signoff_authorized = False
+    for group in request.user.groups.all():
+        if settings.EM_ADVOCATE_GROUP==group.name:
+            advocate_signoff_authorized = True
+            context['signoff_form'] = SignoffForm()
+            instrument = ''
+            try:
+                context['advocate_signoff_object'] = Signoff.objects.get(event=event, 
+                    instrument=instrument, signoff_type='ADV')
+            except:
+                context['advocate_signoff_object'] = None
+            req_label = 'ADVREQ'
+            label_exists = req_label in [l.label.name for l in event.labelling_set.all()]
+
+            context['advocate_signoff_active'] = label_exists or context['advocate_signoff_object']
+
+            break
+    context['advocate_signoff_authorized'] = advocate_signoff_authorized
 
     # Choose your template according to the event's pipeline.
     templates = ['gracedb/event_detail.html',]
@@ -891,41 +913,67 @@ def modify_t90(request, event):
 
 # XXX So this should probably be moved into view_logic anyway.
 from alert import issueXMPPAlert
-from view_utils import operatorSignoffToDict
+from view_utils import signoffToDict
+from models import SIGNOFF_TYPE_CHOICES
+
+def get_signoff_type(stype):
+    for t in SIGNOFF_TYPE_CHOICES:
+        if stype in t:
+            return t[0]
+    return None
 
 @event_and_auth_required
-def modify_operator_signoff(request, event):
-    # Get group_name and action from POST
+def modify_signoff(request, event):
     if not request.method=='POST':
         msg = 'create_operator_signoff only allows POST.'
         return HttpResponseBadRequest(msg)
+    import logging
+    logger = logging.getLogger(__name__)
+    logger.debug("Got POST dict: %s" % request.POST)
 
     authorized = False
-    instrument = None
-    # XXX Note that this may not be the best way to perform the authorization check.
-    # In particular, this assumes that the user can only be a member of one group 
-    # at a time. That should be the case, however, as the control room machines are 
-    # physically well separated and should have different IPs.
-    for group in request.user.groups.all():
-        if '_control_room' in group.name:
+    instrument = ''
+    action = request.POST.get('action', 'create')
+    signoff_type = request.POST.get('signoff_type', 'operator')
+
+    if signoff_type=='operator':
+        # XXX Note that this may not be the best way to perform the authorization check.
+        # In particular, this assumes that the user can only be a member of one group 
+        # at a time. That should be the case, however, as the control room machines are 
+        # physically well separated and should have different IPs.
+        for group in request.user.groups.all():
+            if '_control_room' in group.name:
+                authorized = True
+                instrument = group.name[:2].upper()
+                break
+        if not len(instrument):
+            msg = "Unknown instrument/control room for signoff."
+            return HttpResponseBadRequest(msg)
+
+        req_label = instrument + 'OPS'
+        label_stem = instrument
+    elif signoff_type=='advocate':
+        user_groups = [g.name for g in request.user.groups.all()]
+        if settings.EM_ADVOCATE_GROUP in user_groups:
             authorized = True
-            instrument = group.name[:2].upper()
-            break
+
+        req_label = 'ADVREQ'
+        label_stem = 'ADV'
+    else:
+        msg = 'Unknown signoff type.'
+        return HttpResponseBadRequest(msg)
 
     if not authorized:
-        msg = "You do not appear to be in one of the control rooms."
-        msg += " Therefore, you are not authorized to perform the requested action."
+        msg += "You are not authorized to perform the requested action."
         return HttpResponseForbidden(msg)      
 
-    action = request.POST.get('action', 'create')
-    label_name = instrument + 'OPS'
-
-    existing = OperatorSignoff.objects.filter(event=event, instrument=instrument)
+    existing = Signoff.objects.filter(event=event, instrument=instrument, 
+        signoff_type=get_signoff_type(signoff_type))
     if existing.count() and action=='create':
-        msg = 'Cannot create multiple signoffs for the same event/instrument.'
+        msg = 'Cannot create multiple signoffs for the same event.'
         return HttpResponseBadRequest(msg) 
 
-    f = OperatorSignoffForm(request.POST)
+    f = SignoffForm(request.POST)
     status = None
     comment = None
     if f.is_valid():
@@ -938,21 +986,24 @@ def modify_operator_signoff(request, event):
             return HttpResponseBadRequest(msg)
 
         # Create the OperatorSignoff object.
-        os = OperatorSignoff.objects.create(submitter = request.user,
+        signoff = Signoff.objects.create(submitter = request.user,
             event = event, instrument = instrument, 
-            status = status, comment = comment)
+            status = status, comment = comment,
+            signoff_type = get_signoff_type(signoff_type))
 
-        # Remove the signoff label.
+        # Remove the request label.
         for l in event.labelling_set.all():
-            if l.label.name == label_name:
+            if l.label.name == req_label:
                 l.delete()
 
         # Create a new label.
-        os_label_name = instrument + status
-        create_label(event, os_label_name, request.user, doAlert=False, doXMPP=False)
+        label_name = label_stem + status
+        create_label(event, label_name, request.user, doAlert=False, doXMPP=False)
 
         # Create a log message
-        msg = "Operator certified %s status as %s" % (instrument, status)
+        msg = "%s signoff certified status as %s" % (signoff_type, status)
+        if len(instrument):
+            msg += ' for %s' % instrument        
         if comment:
             msg += ': %s' % comment
         logentry = EventLog.objects.create(event=event, issuer=request.user, comment=msg)
@@ -967,34 +1018,39 @@ def modify_operator_signoff(request, event):
 
         # Issue an alert.
         issueXMPPAlert(event, location='', alert_type="signoff", description=status, 
-            serialized_object = operatorSignoffToDict(os))
+            serialized_object = signoffToDict(signoff))
 
     elif action=='edit':
         # get the existing object
-        os = None
-        if existing.count():
-            os = existing[0]
+        signoff = None
+        if existing.count()==1:
+            signoff = existing[0]
+        elif existing.count()>1:
+            msg = 'Found too many existing signoffs. Something is wrong.'
+            return HttpResponseServerError(msg)
     
-        if not os:
-            msg = 'Could not find existing OperatorSignoff for this event/instrument.'
+        if not signoff:
+            msg = 'Could not find existing signoff for this event/instrument.'
             return HttpResponseBadRequest(msg)
 
         # remove the existing label
-        os_label_name = os.instrument + os.status
+        label_name = label_stem + signoff.status
         for l in event.labelling_set.all():
-            if l.label.name == os_label_name:
+            if l.label.name == label_name:
                 l.delete()
 
         delete = request.POST.get('delete', None)
         if delete:
             # delete the operator signoff object
-            os.delete()
+            signoff.delete()
 
             # also restore the label
-            create_label(event, label_name, request.user)
+            create_label(event, req_label, request.user)
 
             # Create a log message
-            msg = "%s operator deleted signoff status" % instrument
+            msg = "deleted %s signoff status" % signoff_type
+            if len(instrument):
+                msg += ' for %s' % instrument
             logentry = EventLog.objects.create(event=event, issuer=request.user, comment=msg)
 
             # XXX Ugh. Hardcoding tagname here.
@@ -1009,19 +1065,21 @@ def modify_operator_signoff(request, event):
                 msg = "Please select a valid status."
                 return HttpResponseBadRequest(msg)
             # update the values
-            os.status = status
-            os.comment = comment
-            os.save()
+            signoff.status = status
+            signoff.comment = comment
+            signoff.save()
             # Issue an alert.
             issueXMPPAlert(event, location='', alert_type="signoff", description=status, 
-                serialized_object = operatorSignoffToDict(os))
+                serialized_object = signoffToDict(signoff))
 
             # Create a new label.
-            os_label_name = instrument + status
-            create_label(event, os_label_name, request.user, doAlert=False, doXMPP=False)
+            label_name = instrument + status
+            create_label(event, label_name, request.user, doAlert=False, doXMPP=False)
 
             # Create a log message
-            msg = "Operator updated %s status as %s" % (instrument, status)
+            msg = "updated %s signoff status as %s" % (signoff_type, status)
+            if len(instrument):
+                msg += ' for %s' % instrument
             if comment:
                 msg += ': %s' % comment
             logentry = EventLog.objects.create(event=event, issuer=request.user, comment=msg)
@@ -1037,159 +1095,3 @@ def modify_operator_signoff(request, event):
     # Finished. Redirect back to the event.
     return HttpResponseRedirect(reverse("view", args=[event.graceid()]))
 
-#------------------------------------------------------------------------------------------
-# Old Stuff
-#------------------------------------------------------------------------------------------
-#
-# Here is the old stuff we used for the Latest page.
-# Originally, public users could see a version of this page with some
-# fields stripped out. We may still want to do something like that in the 
-# future, but for now, we're actually limiting *which* events a public user
-# can see. 
-#
-#class LimitedEvent():
-#    def __init__(self, event):
-#        self._event = event
-#    def __getattr__(self, attr):
-#        if attr == 'gpstime':
-#            return None
-#        elif attr == 'created':
-#            return self._event.created.replace(second=0)
-#        else:
-#            return getattr(self._event, attr)
-#
-#def latest_limited(request):
-#    return latest(request)
-#
-#def latest(request):
-#    context = {}
-#
-#    if request.method == "GET":
-#        form = SimpleSearchForm(request.GET)
-#    else:
-#        form = SimpleSearchForm(request.POST)
-#
-#    template = 'gracedb/latest.html'
-#    if not request.user or not request.user.is_authenticated():
-#        limit = LimitedEvent
-#        template = 'gracedb/latest_public.html'
-#    else:
-#        limit = lambda x: x
-#
-#    context['form'] = form
-#    context['rawquery'] = request.GET.get('query') or request.POST.get('query') or ""
-#
-#    if form.is_valid():
-#        objects = form.cleaned_data['query']
-#        objects = filter_events_for_user(objects, request.user, 'view')[0:50]
-#        context['objects'] = map(limit, objects)
-#        context['error'] = False
-#    else:
-#        context['error'] = True
-#
-#    return render_to_response(
-#            template,
-#            context,
-#            context_instance=RequestContext(request))
-#
-#
-# XXX This looks interesting. Apparently an old attempt by Brian to make a nice 
-# graphical timeline of events, a la SkyAlert. Or something?
-#def timeline(request):
-#    from utils import gpsToUtc
-#    from django.utils import dateformat
-#
-#    response = HttpResponse(mimetype='application/javascript')
-#    events = []
-#    for event in Event.objects.exclude(group__name="Test").all():
-#        if event.gpstime:
-#            t = dateformat.format(gpsToUtc(event.gpstime), "F j, Y h:i:s")+" UTC"
-#
-#            events.append({
-#                'start': t,
-#                'title': event.get_analysisType_display(),
-#                'description':
-#                    "%s<br/>%s" %(event.get_analysisType_display(),"GPS time:%s"%event.gpstime),
-#                'durationEvent':False,
-#              })
-#    d = {'events': events}
-#    msg = json.dumps(d)
-#    response['Content-length'] = len(msg)
-#    response.write(msg)
-#    return response
-#
-#import re
-#from django.core.mail import mail_admins
-#from buildVOEvent import submitToSkyalert
-#
-#def skyalert_authorized(request):
-#    try:
-#        return u"{0} {1}".format(request.user.first_name, request.user.last_name) in settings.SKYALERT_SUBMITTERS
-#    except:
-#        return Fals
-#
-#def skyalert(request, graceid):
-#    event = Event.getByGraceid(graceid)
-#    createLogEntry = True
-#
-#    if not event.gpstime:
-#        request.session['flash_msg'] = "No GPS time.  Event not suitable for submission to SkyAlert"
-#        return HttpResponseRedirect(reverse(view, args=[graceid]))
-#
-#    if not event.far:
-#        request.session['flash_msg'] = "No FAR.  Event not suitable for submission to SkyAlert"
-#        return HttpResponseRedirect(reverse(view, args=[graceid]))
-#
-#    if not skyalert_authorized(request):
-#        request.session['flash_msg'] = "You are not authorized for SkyAlert submission"
-#        return HttpResponseRedirect(reverse(view, args=[graceid]))
-#
-#    try:
-#        skyalert_response = submitToSkyalert(event)
-#    except Exception, e:
-#        message = "SkyAlert Submission Error"
-#        skyalert_response = ""
-#        # XXX umm.  don't we want to know if this email fails silently?
-#        mail_admins("SkyAlert Submission Error",
-#                    "Event: %s\nException: %s\n" % (graceid, e),
-#                    fail_silently=True)
-#
-#    flashmessage = None
-#    if skyalert_response.find("Success") >= 0:
-#        urlpat = re.compile('https?://[^ ]*')
-#        match = urlpat.search(skyalert_response)
-#        if match:
-#            message = "Submitted to Skyalert: %s" % match.group()
-#            url = match.group()
-#            flashmessage = 'Submitted to Skyalert: %s' % url
-#            message = 'Submitted to Skyalert: <a href="%s">%s</a>' % (url,url)
-#        else:
-#            message = "SkyAlert submission problem.  Cannot parse SkyAlert response."
-#            # XXX umm.  don't we want to know if this email fails silently?
-#            mail_admins("SkyAlert response parsing problem",
-#                        "Event: %s\nSkyAlert Response: %s\n" % (graceid, skyalert_response),
-#                        fail_silently=True)
-#    elif (skyalert_response.find('already') >= 0) or (skyalert_response.find('Duplicate') >= 0):
-#            message = "Event already submitted to SkyAlert"
-#            createLogEntry = False
-#    elif skyalert_response:
-#        message = "Skyalert Submission Failed."
-#        mail_admins("SkyAlert submission failed",
-#                    "Event: %s\nSkyAlert Response: %s\n" % (graceid, skyalert_response),
-#                    fail_silently=True)
-#
-#    request.session['flash_msg'] = flashmessage or message
-#
-#    if createLogEntry:
-#        logentry = EventLog(event=event, issuer=request.ligouser, comment=message)
-#        try:
-#            logentry.save()
-#        except:
-#            # XXX Failed to create log entry for skyalert submission.
-#            # Error message?
-#            pass
-#
-#    return HttpResponseRedirect(reverse(view, args=[graceid]))
-#
-
-
diff --git a/migrations/auth/0008_add_advocates.py b/migrations/auth/0008_add_advocates.py
new file mode 100644
index 000000000..d0e940406
--- /dev/null
+++ b/migrations/auth/0008_add_advocates.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+advocate_usernames = [
+    'branson.stephens@LIGO.ORG',
+]
+
+def add_advocates_group_and_users(apps, schema_editor):
+    from django.conf import settings
+
+    User = apps.get_model("auth", "User")
+    Group = apps.get_model("auth", "Group")
+
+    advocates = Group.objects.create(name=settings.EM_ADVOCATE_GROUP)
+
+    for username in advocate_usernames:
+        try:
+            user = User.objects.get(username=username)
+            advocates.user_set.add(user)
+        except:
+            pass
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('auth', '0007_auto_20150708_1134'),
+    ]
+
+    operations = [
+        migrations.RunPython(add_advocates_group_and_users),
+    ]
diff --git a/settings/default.py b/settings/default.py
index 73cc15a16..96175b10d 100644
--- a/settings/default.py
+++ b/settings/default.py
@@ -47,6 +47,8 @@ EMBB_IGNORE_ADDRESSES = ['Mailer-Daemon@gracedb.phys.uwm.edu',]
 # Added for django 1.7.8
 TEST_RUNNER = 'django.test.runner.DiscoverRunner'
 
+EM_ADVOCATE_GROUP = 'em_advocates'
+
 # 11/18/14. No longer checking XMPP_ALERT_CHANNELS. This is not necessary.
 # If someone sends out an event, an alert should go out. Listerers have the 
 # option to unsubscribe from nodes if so desired.
diff --git a/templates/gracedb/event_detail.html b/templates/gracedb/event_detail.html
index 003559c38..a6699c6fc 100644
--- a/templates/gracedb/event_detail.html
+++ b/templates/gracedb/event_detail.html
@@ -62,19 +62,20 @@
 </div>
 {% endif %}
 
-{% if signoff_authorized and signoff_active %}
+<!-- Here is a section for the control room operator signoff -->
+{% if operator_signoff_authorized and operator_signoff_active %}
     <div class="signoff-area">
     <h2> {{signoff_instrument}} Operator Signoff </h2>
     <p> You are seeing this section because you've connected from a machine that, 
     according to our records, is in the {{signoff_instrument}} control room. 
-    {% if signoff_object %}
+    {% if operator_signoff_object %}
     This event has already been signed off on.
     Use the form below if you wish to edit or delete the record. </p>
-    <form action="{% url "modify_operator_signoff" object.graceid %}" method="post">
+    <form action="{% url "modify_signoff" object.graceid %}" method="post">
         <table>
         <tr><th><label for="id_status">Status:</label></th><td><select id="id_status" name="status">
         <option value="">---------</option>
-        {% if signoff_object.status == "OK" %}
+        {% if operator_signoff_object.status == "OK" %}
         <option value="OK" selected="selected">OKAY</option>
         <option value="NO">NOT OKAY</option>
         {% else %}
@@ -83,8 +84,9 @@
         {% endif %}
         </select></td></tr>
         <tr><th><label for="id_comment">Comment:</label></th><td>
-        <textarea cols="40" id="id_comment" name="comment" rows="10"> {{signoff_object.comment}}
+        <textarea cols="40" id="id_comment" name="comment" rows="10"> {{operator_signoff_object.comment}}
         </textarea></td></tr>
+        <input type="hidden" name="signoff_type" value="operator">
         <input type="hidden" name="action" value="edit">
         <tr> <th> Delete </th> <td> <input type="checkbox" name="delete" value="true"> </td> </tr>
         <tr> <td></td> <td> <input type="submit" value="Submit" class="searchButtonClass"> </td> </tr>
@@ -104,9 +106,10 @@
         event,
     {% endif %}
     was the operating status of the detector basically okay, or not? </p>
-    <form action="{% url "modify_operator_signoff" object.graceid %}" method="post">
+    <form action="{% url "modify_signoff" object.graceid %}" method="post">
         <table>
         {{ signoff_form.as_table }}
+        <input type="hidden" name="signoff_type" value="operator">
         <input type="hidden" name="action" value="create">
         <tr> <td></td> <td> <input type="submit" value="Submit" class="searchButtonClass"> </td> </tr>
         </table>
@@ -115,6 +118,54 @@
     </div>
 {% endif %}
 
+
+<!-- Here is a section for the EM advocate signoffs. -->
+<!-- XXX So much ugliness here. Is there a way to do this with the JS? -->
+{% if advocate_signoff_authorized and advocate_signoff_active %}
+    <div class="signoff-area">
+    <h2> Advocate Signoff </h2>
+    <p> You are seeing this section because you're a designated EM followup 
+    advocate.
+    {% if advocate_signoff_object %}
+    This event has already been signed off on.
+    Use the form below if you wish to edit or delete the record. </p>
+    <form action="{% url "modify_signoff" object.graceid %}" method="post">
+        <table>
+        <tr><th><label for="id_status">Status:</label></th><td><select id="id_status" name="status">
+        <option value="">---------</option>
+        {% if advocate_signoff_object.status == "OK" %}
+        <option value="OK" selected="selected">OKAY</option>
+        <option value="NO">NOT OKAY</option>
+        {% else %}
+        <option value="OK">OKAY</option>
+        <option value="NO" selected="selected">NOT OKAY</option>
+        {% endif %}
+        </select></td></tr>
+        <tr><th><label for="id_comment">Comment:</label></th><td>
+        <textarea cols="40" id="id_comment" name="comment" rows="10"> {{advocate_signoff_object.comment}}
+        </textarea></td></tr>
+        <input type="hidden" name="signoff_type" value="advocate">
+        <input type="hidden" name="action" value="edit">
+        <tr> <th> Delete </th> <td> <input type="checkbox" name="delete" value="true"> </td> </tr>
+        <tr> <td></td> <td> <input type="submit" value="Submit" class="searchButtonClass"> </td> </tr>
+        </table>
+    </form>
+
+    {% else %}
+    This event still requires EM Followup advocate signoff. </p> 
+    <form action="{% url "modify_signoff" object.graceid %}" method="post">
+        <table>
+        {{ signoff_form.as_table }}
+        <input type="hidden" name="signoff_type" value="advocate">
+        <input type="hidden" name="action" value="create">
+        <tr> <td></td> <td> <input type="submit" value="Submit" class="searchButtonClass"> </td> </tr>
+        </table>
+    </form>
+    {% endif %}
+    </div>
+{% endif %}
+
+
 <div class="content-area">
 {% block basic_info %}
 <h2> Basic Info </h2>
diff --git a/templates/gracedb/event_detail_script.js b/templates/gracedb/event_detail_script.js
index 7ae3b13b6..1ae0ca3cf 100644
--- a/templates/gracedb/event_detail_script.js
+++ b/templates/gracedb/event_detail_script.js
@@ -9,7 +9,16 @@ var label_descriptions = {
     LUMIN_NO: "LUMIN No",
     LUMIN_GO: "LUMIN Go",
     DQV: "Data quality veto.",
-    INJ: "Injection occured near this time."
+    INJ: "Injection occured near this time.",
+    ADVREQ: "EM advocate signoff requested.",
+    ADVNO: "EM advocate says event is not okay.",
+    ADVOK: "EM advocate says event is okay.",
+    H1OPS: "H1 operator signoff requested.",
+    H1OK: "H1 operator says event is okay.",
+    H1NO: "H1 operator says event is not okay.",
+    L1OPS: "L1 operator signoff requested.",
+    L1OK: "L1 operator says event is okay.",
+    L1NO: "L1 operator says event is not okay."
 }
 function tooltiptext(name, creator, time) {
     return ( creator + " " + time + "<br/>" + label_descriptions[name] );
-- 
GitLab