From 289f1e19673d4c5e1be38c584e4ebe854f8063cf Mon Sep 17 00:00:00 2001 From: Branson Stephens <branson.stephens@ligo.org> Date: Fri, 7 Aug 2015 16:28:02 -0500 Subject: [PATCH] Changes for operator signoffs: - auth middleware to add/remove user to/from control room group based on IP. - new model for operator signoffs (event, instrument unique_together) - form to create, edit, and delete the operator signoff object - eventlog messages for changes to signoff objects. - OperatorSignoff serializer and LVAlert message - alert operator of outstanding events on landing page. - new labels and migration - Added logic to create/remove H1OK, H1NO, etc. labels. - Exposing the OperatorSignoff List resource through REST interface (GET only). --- gracedb/api.py | 37 ++++ gracedb/forms.py | 8 +- gracedb/migrations/0004_operatorsignoff.py | 27 +++ gracedb/migrations/0005_auto_20150811_0929.py | 18 ++ .../0006_add_operator_signoff_labels.py | 27 +++ gracedb/models.py | 14 ++ gracedb/query.py | 2 +- gracedb/templatetags/timeutil.py | 12 ++ gracedb/urls.py | 1 + gracedb/urls_rest.py | 6 + gracedb/view_utils.py | 8 + gracedb/views.py | 202 +++++++++++++++++- ligoauth/middleware/auth.py | 32 +++ settings/branson.py | 33 +-- settings/default.py | 5 + static/css/style.css | 15 ++ templates/gracedb/event_detail.html | 53 +++++ templates/gracedb/index.html | 15 ++ 18 files changed, 496 insertions(+), 19 deletions(-) create mode 100644 gracedb/migrations/0004_operatorsignoff.py create mode 100644 gracedb/migrations/0005_auto_20150811_0929.py create mode 100644 gracedb/migrations/0006_add_operator_signoff_labels.py diff --git a/gracedb/api.py b/gracedb/api.py index a4a7c5171..0e6987b2d 100644 --- a/gracedb/api.py +++ b/gracedb/api.py @@ -24,6 +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 reverse from translator import handle_uploaded_data @@ -1465,6 +1466,9 @@ 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}") + # XXX Need a template for the tag list? templates = { @@ -1478,6 +1482,7 @@ class GracedbRoot(APIView): "filemeta-template" : filemeta, "tag-template" : tag, "taglist-template" : taglist, + "operatorsignoff-list-template": operatorsignofflist, } return Response({ @@ -1835,3 +1840,35 @@ class VOEventDetail(APIView): status=status.HTTP_404_NOT_FOUND) return Response(voeventToDict(voevent, request=request)) +#================================================================== +# OperatorSignoff + +class OperatorSignoffList(APIView): + """Operator Signoff List Resource + + At present, this only supports GET + """ + authentication_classes = (LigoAuthentication,) + permission_classes = (IsAuthenticated,IsAuthorizedForEvent,) + throttle_classes = (AnnotationThrottle,) + + @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() ] + + rv = { + 'start': 0, + 'numRows' : count, + 'links' : { + 'self' : request.build_absolute_uri(), + 'first' : request.build_absolute_uri(), + 'last' : request.build_absolute_uri(), + }, + 'operator_signoff' : operator_signoff, + } + return Response(rv) + diff --git a/gracedb/forms.py b/gracedb/forms.py index cc4a1a2a9..6cbe08de5 100644 --- a/gracedb/forms.py +++ b/gracedb/forms.py @@ -3,9 +3,10 @@ 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 +from models import Pipeline, Search, OperatorSignoff from django.contrib.auth.models import User from django.core.exceptions import FieldError +from django.forms import ModelForm from query import parseQuery from pyparsing import ParseException @@ -79,3 +80,8 @@ class EventSearchForm(forms.Form): labels = forms.MultipleChoiceField(choices=labelChoices, required=False) get_neighbors = forms.BooleanField(required=False) + +class OperatorSignoffForm(ModelForm): + class Meta: + model = OperatorSignoff + fields = [ 'status', 'comment' ] diff --git a/gracedb/migrations/0004_operatorsignoff.py b/gracedb/migrations/0004_operatorsignoff.py new file mode 100644 index 000000000..007ba6d82 --- /dev/null +++ b/gracedb/migrations/0004_operatorsignoff.py @@ -0,0 +1,27 @@ +# -*- 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', '0003_grbevent_trigger_id'), + ] + + operations = [ + migrations.CreateModel( + name='OperatorSignoff', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('instrument', models.CharField(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)), + ], + ), + ] diff --git a/gracedb/migrations/0005_auto_20150811_0929.py b/gracedb/migrations/0005_auto_20150811_0929.py new file mode 100644 index 000000000..49cee514b --- /dev/null +++ b/gracedb/migrations/0005_auto_20150811_0929.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('gracedb', '0004_operatorsignoff'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='operatorsignoff', + unique_together=set([('event', 'instrument')]), + ), + ] diff --git a/gracedb/migrations/0006_add_operator_signoff_labels.py b/gracedb/migrations/0006_add_operator_signoff_labels.py new file mode 100644 index 000000000..8e7009612 --- /dev/null +++ b/gracedb/migrations/0006_add_operator_signoff_labels.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def add_labels(apps, schema_editor): + ifos = ['H1', 'L1'] + labels = { + 'OPS': 'black', + 'OK': 'green', + 'NO': 'red', + } + Label = apps.get_model('gracedb', 'Label') + for ifo in ifos: + for name, color in labels.iteritems(): + label_name = ifo + name + Label.objects.create(name=label_name, defaultColor=color) + +class Migration(migrations.Migration): + + dependencies = [ + ('gracedb', '0005_auto_20150811_0929'), + ] + + operations = [ + migrations.RunPython(add_labels) + ] diff --git a/gracedb/models.py b/gracedb/models.py index 032409f09..e9b47a6fb 100644 --- a/gracedb/models.py +++ b/gracedb/models.py @@ -1096,3 +1096,17 @@ class VOEvent(models.Model): # in the views that use it and give an informative error message. raise Exception("Too many attempts to save log message. Something is wrong.") +INSTRUMENTS = ( ('H1', 'LHO'), ('L1', 'LLO'), ('V1', 'Virgo') ) +OPERATOR_STATUSES = ( ('OK', 'OKAY'), ('NO', 'NOT OKAY') ) +class OperatorSignoff(models.Model): + class Meta: + unique_together = ("event","instrument") + submitter = models.ForeignKey(DjangoUser) + event = models.ForeignKey(Event) + instrument = models.CharField(max_length=2, choices=INSTRUMENTS) + status = models.CharField(max_length=2, choices=OPERATOR_STATUSES, blank=False) + comment = models.TextField(blank=True) + + def __unicode__(self): + return "%s | %s | %s" % (self.event.graceid(), self.instrument, self.status) + diff --git a/gracedb/query.py b/gracedb/query.py index caceacda5..3015d13c4 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"] +labelNames = ["DQV", "INJ", "LUMIN_NO", "LUMIN_GO", "SWIFT_NO", "SWIFT_GO", "EM_READY", "cWB_r","cWB_s", "H1OPS", "L1OPS", "V1OPS"] label = Or([CaselessLiteral(n) for n in labelNames]).\ setParseAction( lambda toks: Q(labels__name=toks[0]) ) diff --git a/gracedb/templatetags/timeutil.py b/gracedb/templatetags/timeutil.py index 0e65e56ff..3eaee57bd 100644 --- a/gracedb/templatetags/timeutil.py +++ b/gracedb/templatetags/timeutil.py @@ -131,6 +131,18 @@ def utc(dt, format=FORMAT): def gpsdate(gpstime, format=FORMAT): return dateformat.format(gpsToUtc(gpstime), format) +@register.filter +def gpsdate_tz(gpstime, label="utc"): + format = FORMAT + dt = gpsToUtc(gpstime) + if label=='lho': + dt = dt.astimezone(LHO_TZ) + elif label=='llo': + dt = dt.astimezone(LLO_TZ) + elif label=='virgo': + dt = dt.astimezone(VIRGO_TZ) + return dateformat.format(dt, format) + @register.filter def gpstime(dt): if not dt.tzinfo: diff --git a/gracedb/urls.py b/gracedb/urls.py index aedfb0bd6..1ddbe3e2c 100644 --- a/gracedb/urls.py +++ b/gracedb/urls.py @@ -19,6 +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+)/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 2ed93e93b..048550373 100644 --- a/gracedb/urls_rest.py +++ b/gracedb/urls_rest.py @@ -21,6 +21,7 @@ from gracedb.api import EventPermissionList from gracedb.api import GroupEventPermissionList from gracedb.api import GroupEventPermissionDetail from gracedb.api import VOEventList, VOEventDetail +from gracedb.api import OperatorSignoffList urlpatterns = patterns('gracedb.api', @@ -104,6 +105,11 @@ urlpatterns = patterns('gracedb.api', url (r'^events/(?P<graceid>\w[\d]+)/neighbors/$', EventNeighbors.as_view(), name="neighbors"), + # Operator Signoff Resources + url (r'events/(?P<graceid>[GEHMT]\d+)/signoff/$', + OperatorSignoffList.as_view(), name='operatorsignoff-list'), + + # Performance stats url (r'^performance/$', PerformanceInfo.as_view(), name='performance-info'), diff --git a/gracedb/view_utils.py b/gracedb/view_utils.py index bb2992583..70b3d5167 100644 --- a/gracedb/view_utils.py +++ b/gracedb/view_utils.py @@ -526,6 +526,14 @@ def singleInspiralToDict(single_inspiral): rv.update({ field_name: value }) return rv +def operatorSignoffToDict(operator_signoff): + return { + 'submitter': operator_signoff.submitter.username, + 'instrument': operator_signoff.instrument, + 'status': operator_signoff.status, + 'comment': operator_signoff.comment, + } + #--------------------------------------------------------------------------------------- #--------------------------------------------------------------------------------------- # Miscellany diff --git a/gracedb/views.py b/gracedb/views.py index 3496f0441..63adf4e55 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 -from forms import CreateEventForm, EventSearchForm, SimpleSearchForm +from models import EMGroup, OperatorSignoff +from forms import CreateEventForm, EventSearchForm, SimpleSearchForm, OperatorSignoffForm from django.contrib.auth.models import User, Permission from django.contrib.auth.models import Group as AuthGroup @@ -22,6 +22,7 @@ from view_logic import get_performance_info from view_logic import get_lvem_perm_status from view_logic import create_eel from view_logic import create_emobservation +from view_logic import create_label from view_utils import assembleLigoLw, get_file from view_utils import flexigridResponse, jqgridResponse @@ -63,9 +64,29 @@ def event_and_auth_required(view): def index(request): # assert request.user - return render_to_response( - 'gracedb/index.html', - {}, + context = {} + + signoff_authorized = False + signoff_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: + signoff_authorized = True + signoff_instrument = group.name[:2].upper() + break + + context['signoff_authorized'] = signoff_authorized + context['signoff_instrument'] = signoff_instrument + + if signoff_authorized: + label_name = signoff_instrument + 'OPS' + events = Event.objects.filter(labelling__label__name=label_name) + context['signoff_graceids'] = [e.graceid() for e in events] + + return render_to_response('gracedb/index.html', context, context_instance=RequestContext(request)) def navbar_only(request): @@ -301,6 +322,30 @@ 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 + # 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 + context['signoff_instrument'] = group.name[:2].upper() + context['signoff_form'] = OperatorSignoffForm() + instrument = group.name[:2].upper() + try: + context['signoff_object'] = OperatorSignoff.objects.get(event=event, instrument=instrument) + 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['signoff_active'] = label_exists or context['signoff_object'] + + break + context['signoff_authorized'] = signoff_authorized + # Choose your template according to the event's pipeline. templates = ['gracedb/event_detail.html',] if event.pipeline.name in settings.COINC_PIPELINES: @@ -842,6 +887,153 @@ def modify_t90(request, event): # Finished. Redirect back to the event. return HttpResponseRedirect(reverse("view", args=[event.graceid()])) +# XXX So this should probably be moved into view_logic anyway. +from alert import issueXMPPAlert +from view_utils import operatorSignoffToDict + +@event_and_auth_required +def modify_operator_signoff(request, event): + # Get group_name and action from POST + if not request.method=='POST': + msg = 'create_operator_signoff only allows POST.' + return HttpResponseBadRequest(msg) + + 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: + authorized = True + instrument = group.name[:2].upper() + break + + 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." + return HttpResponseForbidden(msg) + + action = request.POST.get('action', 'create') + label_name = instrument + 'OPS' + + existing = OperatorSignoff.objects.filter(event=event, instrument=instrument) + if existing.count() and action=='create': + msg = 'Cannot create multiple signoffs for the same event/instrument.' + return HttpResponseBadRequest(msg) + + f = OperatorSignoffForm(request.POST) + status = None + comment = None + if f.is_valid(): + status = f.cleaned_data['status'] + comment = f.cleaned_data['comment'] + + if action=='create': + if status==None: + msg = "Please select a valid status." + return HttpResponseBadRequest(msg) + + # Create the OperatorSignoff object. + os = OperatorSignoff.objects.create(submitter = request.user, + event = event, instrument = instrument, + status = status, comment = comment) + + # Remove the signoff label. + for l in event.labelling_set.all(): + if l.label.name == label_name: + l.delete() + + # Create a new label. + os_label_name = instrument + status + create_label(event, os_label_name, request.user, doAlert=False, doXMPP=False) + + # Create a log message + msg = "Operator certified %s status as %s" % (instrument, status) + if comment: + msg += ': %s' % comment + logentry = EventLog.objects.create(event=event, issuer=request.user, comment=msg) + + # XXX Ugh. Hardcoding tagname here. + # Add a tag to the log message + try: + tag = Tag.objects.get(name='em_follow') + tag.eventlogs.add(logentry) + except: + pass + + # Issue an alert. + issueXMPPAlert(event, location='', alert_type="signoff", description=status, + serialized_object = operatorSignoffToDict(os)) + + elif action=='edit': + # get the existing object + os = None + if existing.count(): + os = existing[0] + + if not os: + msg = 'Could not find existing OperatorSignoff for this event/instrument.' + return HttpResponseBadRequest(msg) + + # remove the existing label + os_label_name = os.instrument + os.status + for l in event.labelling_set.all(): + if l.label.name == os_label_name: + l.delete() + + delete = request.POST.get('delete', None) + if delete: + # delete the operator signoff object + os.delete() + + # also restore the label + create_label(event, label_name, request.user) + + # Create a log message + msg = "%s operator deleted signoff status" % instrument + logentry = EventLog.objects.create(event=event, issuer=request.user, comment=msg) + + # XXX Ugh. Hardcoding tagname here. + # Add a tag to the log message + try: + tag = Tag.objects.get(name='em_follow') + tag.eventlogs.add(logentry) + except: + pass + else: + if status==None: + msg = "Please select a valid status." + return HttpResponseBadRequest(msg) + # update the values + os.status = status + os.comment = comment + os.save() + # Issue an alert. + issueXMPPAlert(event, location='', alert_type="signoff", description=status, + serialized_object = operatorSignoffToDict(os)) + + # Create a new label. + os_label_name = instrument + status + create_label(event, os_label_name, request.user, doAlert=False, doXMPP=False) + + # Create a log message + msg = "Operator updated %s status as %s" % (instrument, status) + if comment: + msg += ': %s' % comment + logentry = EventLog.objects.create(event=event, issuer=request.user, comment=msg) + + # XXX Ugh. Hardcoding tagname here. + # Add a tag to the log message + try: + tag = Tag.objects.get(name='em_follow') + tag.eventlogs.add(logentry) + except: + pass + + # Finished. Redirect back to the event. + return HttpResponseRedirect(reverse("view", args=[event.graceid()])) #------------------------------------------------------------------------------------------ # Old Stuff diff --git a/ligoauth/middleware/auth.py b/ligoauth/middleware/auth.py index 444bec387..52702f43c 100644 --- a/ligoauth/middleware/auth.py +++ b/ligoauth/middleware/auth.py @@ -29,6 +29,14 @@ PUBLIC_URLS = [ '/DiscoveryService/', ] +def get_client_ip(request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + def cert_dn_from_request(request): """Take a request, rummage through SSL_* headers, return the DN for the user.""" certdn = request.META.get('SSL_CLIENT_S_DN') @@ -143,6 +151,17 @@ class LigoAuthMiddleware: request.user = user + # If the user is connecting from one of the control rooms, add him/her to + # the appropriate control room group. But let's not do this if api + # is in the path. + if user and 'api' not in request.path_info: + user_ip = get_client_ip(request) + for ifo, ip in settings.CONTROL_ROOM_IPS.iteritems(): + if ip == user_ip: + group_name = ifo.lower() + '_control_room' + group = Group.objects.get(name=group_name) + group.user_set.add(user) + # Check: Is the requested URL allowed for the PUBLIC? #if user is None: if user is None and request.path_info not in PUBLIC_URLS: @@ -163,6 +182,19 @@ class LigoAuthMiddleware: {'error': message}, status=403, context_instance=RequestContext(request)) + def process_response(self, request, response): + # If the user is connecting from one of the control rooms, remove him/her from + # the appropriate control room group + user = getattr(request, 'user', None) + if user: + user_ip = get_client_ip(request) + for ifo, ip in settings.CONTROL_ROOM_IPS.iteritems(): + if ip == user_ip: + group_name = ifo.lower() + '_control_room' + group = Group.objects.get(name=group_name) + group.user_set.remove(request.user) + return response + class RemoteUserBackend(DefaultRemoteUserBackend): create_unknown_user = False diff --git a/settings/branson.py b/settings/branson.py index 63337b573..d225d95cb 100644 --- a/settings/branson.py +++ b/settings/branson.py @@ -2,7 +2,7 @@ CONFIG_NAME = "Branson" DEBUG = True TEMPLATE_DEBUG = DEBUG -DEBUG_TOOLBAR_PATH_SETTINGS = False +DEBUG_TOOLBAR_PATCH_SETTINGS = False DATABASES = { 'default' : { @@ -45,15 +45,19 @@ GRACEDB_DATA_DIR = "/home/branson/new_fake_data" EMAIL_HOST = 'localhost' ALERT_EMAIL_FROM = "Dev Alert <root@moe.phys.uwm.edu>" -ALERT_EMAIL_TO = [ - "Branson Stephens <branson@gravity.phys.uwm.edu>", - ] -ALERT_EMAIL_BCC = ["branson@gravity.phys.uwm.edu"] - +#ALERT_EMAIL_TO = [ +# "Branson Stephens <branson@gravity.phys.uwm.edu>", +# ] +#ALERT_EMAIL_BCC = ["branson@gravity.phys.uwm.edu"] +# +#ALERT_TEST_EMAIL_FROM = "Dev Test Alert <root@moe.phys.uwm.edu>" +#ALERT_TEST_EMAIL_TO = [ +# "Branson Stephens <branson@gravity.phys.uwm.edu>", +# ] +ALERT_EMAIL_TO = [] +ALERT_EMAIL_BCC = [] ALERT_TEST_EMAIL_FROM = "Dev Test Alert <root@moe.phys.uwm.edu>" -ALERT_TEST_EMAIL_TO = [ - "Branson Stephens <branson@gravity.phys.uwm.edu>", - ] +ALERT_TEST_EMAIL_TO = [] ALERT_XMPP_SERVERS = ["lvalert-test.cgca.uwm.edu",] LVALERT_SEND_EXECUTABLE = '/home/branson/djangoenv/bin/lvalert_send' @@ -103,7 +107,7 @@ MIDDLEWARE_CLASSES = [ 'django.contrib.messages.middleware.MessageMiddleware', 'ligoauth.middleware.auth.LigoAuthMiddleware', #'debug_toolbar.middleware.DebugToolbarMiddleware', - #'debug_panel.middleware.DebugPanelMiddleware', + 'debug_panel.middleware.DebugPanelMiddleware', 'middleware.profiling.ProfileMiddleware', ] @@ -118,14 +122,19 @@ INSTALLED_APPS = ( 'ligoauth', 'rest_framework', 'guardian', - #'debug_toolbar', - #'debug_panel', + 'debug_toolbar', + 'debug_panel', ) INTERNAL_IPS = ( '129.89.57.83', ) +CONTROL_ROOM_IPS = { + 'H1': '108.45.69.217', +# 'L1': '129.2.92.124', + 'L1': '129.89.57.83', +} # Settings for Logging. import logging diff --git a/settings/default.py b/settings/default.py index 46d4d5d24..7ef508c5a 100644 --- a/settings/default.py +++ b/settings/default.py @@ -345,6 +345,11 @@ SOUTH_TESTS_MIGRATE = False # passwords for LVEM scripted access expire after 365 days. PASSWORD_EXPIRATION_TIME = timedelta(days=365) +CONTROL_ROOM_IPS = { + 'H1': '129.89.57.83', + 'L1': '129.2.92.124', +} + # XXX The following Log settings are for a performance metric. import logging LOG_ROOT = '/home/gracedb/logs' diff --git a/static/css/style.css b/static/css/style.css index bf36b7ac5..319108ec7 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -27,6 +27,21 @@ table.gstlalcbc th {padding:3px;border:none;vertical-align:bottom;} margin-right: 15px; } +.signoff-area { + margin-top: 10px; + margin-bottom: 30px; + margin-left: 15px; + /*margin-right: 15px; */ + max-width: 600px; + background-color: #ffe6e6; + /* background-color: #fff0f0; */ + padding: 4px; +} + +.signoff-area th { + background-color: rgb(200, 200, 200); +} + table.figures tr.figrow {text-align:center;} table.figures {width:300px;height:270px;border:1px solid gray; display: inline-block; margin: 4px; overflow: hidden;} diff --git a/templates/gracedb/event_detail.html b/templates/gracedb/event_detail.html index 5727645c1..003559c38 100644 --- a/templates/gracedb/event_detail.html +++ b/templates/gracedb/event_detail.html @@ -62,6 +62,59 @@ </div> {% endif %} +{% if signoff_authorized and 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 %} + 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"> + <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" %} + <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"> {{signoff_object.comment}} + </textarea></td></tr> + <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 operator signoff. </p> + <p> Please answer the following (and optionally enter a comment): At the time of the + {% if signoff_instrument == 'H1' %} + event ({{ object.gpstime|gpsdate_tz:"lho" }}), + {% elif signoff_instrument == 'L1' %} + event ({{ object.gpstime|gpsdate_tz:"llo" }}), + {% elif signoff_instrument == 'V1' %} + event ({{ object.gpstime|gpsdate_tz:"virgo" }}), + {% else %} + event, + {% endif %} + was the operating status of the detector basically okay, or not? </p> + <form action="{% url "modify_operator_signoff" object.graceid %}" method="post"> + <table> + {{ signoff_form.as_table }} + <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/index.html b/templates/gracedb/index.html index 053de0e76..de236f43e 100644 --- a/templates/gracedb/index.html +++ b/templates/gracedb/index.html @@ -50,5 +50,20 @@ follow-ups. </p> </div> +<!--XXX Infrastructure for human signoffs. Hopefully this will not be permanent. --> +{% if signoff_authorized %} + {%if signoff_graceids|length %} + <div class="signoff-area"> + <h2>Attention {{signoff_instrument}} Operator</h2> + <p> The following events require your sign-off: </p> + <ul> + {% for graceid in signoff_graceids %} + <li> <a href="{% url "view" graceid %}">{{ graceid }}</a></li> + {% endfor %} + </ul> + <p> Please click on the link(s) above and use the form at the top of the page. </p> + </div> + {% endif %} +{% endif %} {% endblock %} -- GitLab