...
 
Commits (13)
......@@ -58,8 +58,6 @@ before_script:
libxml2-dev
swig
${PYTHON}-pip
# install voeventlib for python2
- if [[ "${PYTHON_MAJOR}" -eq 2 ]]; then apt-get -o dir::cache::archives="${APT_CACHE_DIR}" install -yqq python-voeventlib; fi
# install everything else from pip
- ${PYTHON} -m pip install -r requirements.txt
# create logs path required for tests
......
......@@ -28,7 +28,6 @@ RUN apt-get update && \
python2.7-dev \
python-libxml2 \
python-pip \
python-voeventlib \
procps \
shibboleth \
supervisor \
......
......@@ -33,6 +33,7 @@ ALLOWED_HOSTS += ['testserver']
# Home page stuff
INSTANCE_TITLE = 'GraceDB Playground'
INSTANCE_INFO = """
<h3>Playground instance</h3>
<p>
This GraceDB instance is designed for users to develop and test their own
applications. It mimics the production instance in all but the following ways:
......
import logging
from rest_framework import permissions
# Set up logger
logger = logging.getLogger(__name__)
class CanUpdateGrbEvent(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.has_perm('events.t90_grbevent')
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
from django.db.models import Q
from django.urls import reverse
from rest_framework.test import APIRequestFactory as rf
from events.models import GrbEvent
from ..views import GrbEventPatchView
from ...settings import API_VERSION
def v_reverse(viewname, *args, **kwargs):
"""Easily customizable versioned API reverse for testing"""
viewname = 'api:{version}:'.format(version=API_VERSION) + viewname
return reverse(viewname, *args, **kwargs)
def test_access(mock_internal_user):
# Create a mock event
mock_event = mock.MagicMock(spec=GrbEvent, graceid='E1234')
mock_event.search.name = 'GRB'
mock_event.redshift = 3
# Get URL and set up request
url = v_reverse("events:update-grbevent", args=[mock_event.graceid])
data = {'redshift': 2}
request = rf().patch(url, data=data)
request.user = mock_internal_user
view = GrbEventPatchView.as_view()
with mock.patch('api.v1.events.views.EventAlertIssuer'), \
mock.patch('api.v1.events.views.Event.getByGraceid') as mock_get_event, \
mock.patch('api.v1.events.views.eventToDict'):
mock_get_event.return_value = mock_event
response = view(request, mock_event.graceid)
response.render()
assert mock_get_event.call_args[0] == (mock_event.graceid,)
......@@ -9,6 +9,8 @@ urlpatterns = [
url(r'^$', EventList.as_view(), name='event-list'),
url(r'^(?P<graceid>[GEHMT]\d+)$', EventDetail.as_view(),
name='event-detail'),
url(r'^(?P<graceid>[GEHMT]\d+)/update-grbevent/$',
GrbEventPatchView.as_view(), name='update-grbevent'),
# Event Log Resources
# events/{graceid}/logs/[{logid}]
......
......@@ -26,7 +26,9 @@ from glue.ligolw.ligolw import LIGOLWContentHandler
from glue.ligolw.lsctables import use_in
from guardian.models import GroupObjectPermission
from rest_framework import authentication, parsers, serializers, status
from rest_framework.permissions import IsAuthenticated, BasePermission, SAFE_METHODS
from rest_framework.exceptions import ValidationError as DrfValidationError
from rest_framework.permissions import IsAuthenticated, BasePermission, \
SAFE_METHODS
from rest_framework.renderers import BaseRenderer, JSONRenderer, \
BrowsableAPIRenderer
from rest_framework.response import Response
......@@ -40,7 +42,7 @@ from core.vfile import VersionedFile
from events.buildVOEvent import buildVOEvent, VOEventBuilderException
from events.forms import CreateEventForm
from events.models import Event, Group, Search, Pipeline, EventLog, Tag, \
Label, EMGroup, EMBBEventLog, EMSPECTRUM, VOEvent
Label, Labelling, EMGroup, EMBBEventLog, EMSPECTRUM, VOEvent, GrbEvent
from events.permission_utils import user_has_perm, filter_events_for_user, \
is_external, check_external_file_access
from events.translator import handle_uploaded_data
......@@ -53,6 +55,7 @@ from events.view_utils import eventToDict, eventLogToDict, labelToDict, \
from search.forms import SimpleSearchForm
from search.query.events import parseQuery, ParseException
from superevents.models import Superevent
from .permissions import CanUpdateGrbEvent
from .throttling import EventCreationThrottle, AnnotationThrottle
from ..mixins import InheritDefaultPermissionsMixin
from ...utils import api_reverse
......@@ -82,7 +85,7 @@ class IsAuthorizedForEvent(BasePermission):
# "Unsafe methods" require change permissions on the event.
# Note that DELETE is only implemented for event-log-tag
# relationships.
elif request.method in ['PUT','POST','DELETE']:
elif request.method in ['PUT', 'PATCH', 'POST', 'DELETE']:
shortname = 'change'
else:
return False
......@@ -637,6 +640,81 @@ class EventDetail(InheritPermissionsAPIView):
return Response(status=status.HTTP_202_ACCEPTED)
# New class *only* for updating GRB event properties
class GrbEventPatchView(InheritPermissionsAPIView):
permission_classes = (IsAuthenticated, IsAuthorizedForEvent,
CanUpdateGrbEvent)
updatable_attributes = ['t90', 'redshift', 'designation', 'ra', 'dec',
'error_radius']
def process_data(self, data):
return {k: (float(v) if k != 'designation' else v)
for k, v in data.items()}
def get_attributes_to_update(self, grbevent, data):
attrib_to_update = [k for k in data if (k in self.updatable_attributes
and getattr(grbevent, k, None) != data[k])]
# If none, raise an error
if not attrib_to_update:
raise DrfValidationError('Request would not modify the GRB event')
return {k: data[k] for k in attrib_to_update}
def generate_log_message(self, grbevent, update_dict):
# Templates
comment = "Updated GRB event parameters: {msg}"
param_template = "{name}: {old} -> {new}"
# Message strings for updated parameters
update_list = [
param_template.format(
name=k,
old=getattr(grbevent, k),
new=update_dict[k]
)
for k in update_dict
]
return comment.format(msg=", ".join(update_list))
@event_and_auth_required
def patch(self, request, grbevent):
# grbevent here should be a GrbEvent due to the way
# event_and_auth_required works
# Make sure this is a GRB event
if (grbevent.search is None
or (grbevent.search and grbevent.search.name != 'GRB')
or not isinstance(grbevent, GrbEvent)):
msg = ("Cannot update GRB event parameters for non-GRB event "
"{gid}").format(gid=grbevent.graceid)
return Response(msg, status=status.HTTP_400_BAD_REQUEST)
# Process data - should be all floats except designation
data = self.process_data(request.data)
# Get attributes to update and their values
update_dict = self.get_attributes_to_update(grbevent, data)
# Generate log message before updating event
update_message = self.generate_log_message(grbevent, update_dict)
# Update the event
for attribute in update_dict:
setattr(grbevent, attribute, update_dict[attribute])
grbevent.save()
# Save log message
grbevent.eventlog_set.create(comment=update_message,
issuer=request.user)
# Send LVAlert
EventAlertIssuer(grbevent, alert_type='update').issue_alerts()
return Response(eventToDict(grbevent, request=request))
#==================================================================
# Neighbors
......@@ -730,11 +808,13 @@ class EventLabel(InheritPermissionsAPIView):
@event_and_auth_required
def delete(self, request, event, label):
try:
rv = delete_label(event, request, label)
except Labelling.DoesNotExist as e:
return Response(e.message, status=status.HTTP_404_NOT_FOUND)
except (ValueError, Label.ProtectedLabelError) as e:
return Response(e.message,
status=status.HTTP_400_BAD_REQUEST)
return Response(e.message, status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
......
......@@ -103,6 +103,10 @@ class GracedbRoot(APIView):
signofflist = api_reverse("events:signoff-list", args=["G1200"], request=request)
signofflist = signofflist.replace("G1200", "{graceid}")
update_grbevent = api_reverse("events:update-grbevent", args=["G1200"],
request=request)
update_grbevent = update_grbevent.replace("G1200", "{graceid}")
# XXX Need a template for the tag list?
templates = {
......@@ -119,6 +123,7 @@ class GracedbRoot(APIView):
"tag-template" : tag,
"taglist-template" : taglist,
"signoff-list-template": signofflist,
"update-grbevent-template": update_grbevent,
}
# Get superevent templates
......
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
......@@ -5,6 +9,9 @@ from django.contrib.auth.models import (
Group, Permission, AnonymousUser,
)
from django_mock_queries.query import MockSet
# Groups ----------------------------------------------------------------------
@pytest.mark.django_db
@pytest.fixture
......@@ -89,6 +96,7 @@ def em_advocate_user(django_user_model, internal_group, em_advocates_group):
return user
# User lists ------------------------------------------------------------------
@pytest.fixture(params=['internal_user', 'public_user'])
def standard_user(request):
......@@ -97,3 +105,43 @@ def standard_user(request):
internal user, public user (LV-EM user to come?)
"""
return request.getfixturevalue(request.param)
# Attempts at mocking things away
## Mock groups ------------------------
@pytest.fixture
def mock_internal_group():
group = mock.MagicMock(spec=Group)
group.permissions = MockSet()
group.name = settings.LVC_GROUP
# Create mock permissions
perm_data = [
{'content_type.app.label': 'superevents', 'codename': 'add_labelling'},
{'content_type.app.label': 'superevents', 'codename': 'delete_labelling'},
{'content_type.app.label': 'superevents', 'codename': 'tag_log'},
{'content_type.app.label': 'superevents', 'codename': 'untag_log'},
{'content_type.app.label': 'superevents', 'codename': 'view_log'},
{'content_type.app.label': 'superevents', 'codename': 'add_test_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'change_test_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'confirm_gw_test_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'annotate_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'view_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'add_voevent'},
{'content_type.app.label': 'superevents', 'codename': 'view_supereventgroupobjectpermission'},
{'content_type.app.label': 'superevents', 'codename': 'view_signoff'},
]
for perm in perm_data:
p = mock.MagicMock(spec=Permission, **perm)
group.permissions.add(p)
return group
## Mock user objects ------------------
@pytest.fixture
def mock_internal_user(django_user_model, mock_internal_group):
user = mock.MagicMock(spec=django_user_model)
user.groups = MockSet()
user.groups.add(mock_internal_group)
return user
......@@ -3,7 +3,7 @@ import logging
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 Event, Group, Label, GrbEvent
from .models import Pipeline, Search, Signoff
from django.contrib.auth.models import User
from django.core.exceptions import FieldError
......@@ -49,3 +49,10 @@ class SignoffForm(ModelForm):
class Meta:
model = Signoff
fields = [ 'status', 'comment' ]
class GrbEventUpdateForm(ModelForm):
class Meta:
model = GrbEvent
fields = ['ra', 'dec', 'error_radius', 't90', 'redshift',
'designation']
from django.db import models
# Custom managers for the Pipeline model --------------------------------------
class ProductionPipelineManager(models.Manager):
"""Pipelines which are production search pipelines"""
def get_queryset(self):
return super(ProductionPipelineManager, self).get_queryset().filter(
pipeline_type=self.model.PIPELINE_TYPE_SEARCH_PRODUCTION
)
class ExternalPipelineManager(models.Manager):
"""Pipelines which correspond to external experiments"""
def get_queryset(self):
return super(ExternalPipelineManager, self).get_queryset().filter(
pipeline_type=self.model.PIPELINE_TYPE_EXTERNAL
)
......@@ -89,19 +89,34 @@ def populate_values(voevent, event_or_superevent):
# Parse parameters
for parameter in PARAMETER_MAPPINGS:
result = None
for path in PARAMETER_MAPPINGS[parameter]:
result = None
try:
result = root.find(path)
except SyntaxError as e:
pass
else:
# If result is not None, we've found something, so let's break
if result is not None:
break
if result is None:
# not found, likely due to an old VOEvent schema
continue
# Special parameter parsing
if parameter in ['hardware_inj', 'internal', 'open_alert']:
value = bool(int(result.attrib['value']))
# Better bool processing
try:
value = bool(int(result.attrib['value']))
except ValueError:
value = result.attrib['value']
if value.lower() in ['f', '0', 'false']:
value = False
elif value.lower() in ['t', '1', 'true']:
value = True
else:
raise ValueError("Can't process value {0}".format(value))
elif parameter == 'skymap_filename':
value = result.attrib['value'].split('/')[-1]
elif parameter == 'skymap_type':
......@@ -118,9 +133,10 @@ def populate_values(voevent, event_or_superevent):
for parameter, str_list in DESCRIPTION_PARAMETER_MAPPINGS.items():
for s in str_list:
if any([s in dp for dp in desc_text]):
setattr(voevent, parameter, True)
value = True
else:
setattr(voevent, parameter, False)
value = False
setattr(voevent, parameter, value)
# Save VOEvent
voevent.save()
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-10 19:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0037_delete_approval_model'),
]
operations = [
migrations.AddField(
model_name='pipeline',
name='pipeline_type',
field=models.CharField(choices=[(b'E', b'external'), (b'O', b'other'), (b'SO', b'non-production search'), (b'SP', b'production search')], default='O', max_length=2),
preserve_default=False,
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-10 19:34
from __future__ import unicode_literals
from django.db import migrations
from events.models import Pipeline as pipeline_model
PIPELINES = [
('CWB', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('MBTAOnline', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('gstlal', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('oLIB', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('pycbc', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('spiir', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('CWB2G', pipeline_model.PIPELINE_TYPE_SEARCH_OTHER),
('Ringdown', pipeline_model.PIPELINE_TYPE_SEARCH_OTHER),
('X', pipeline_model.PIPELINE_TYPE_SEARCH_OTHER),
('HardwareInjection', pipeline_model.PIPELINE_TYPE_OTHER),
('Omega', pipeline_model.PIPELINE_TYPE_OTHER),
('Q', pipeline_model.PIPELINE_TYPE_OTHER),
('Fermi', pipeline_model.PIPELINE_TYPE_EXTERNAL),
('SNEWS', pipeline_model.PIPELINE_TYPE_EXTERNAL),
('Swift', pipeline_model.PIPELINE_TYPE_EXTERNAL),
]
DEFAULT_PIPELINE_TYPE = pipeline_model.PIPELINE_TYPE_OTHER
def update_pipeline_types(apps, schema_editor):
Pipeline = apps.get_model('events', 'Pipeline')
for p_tuple in PIPELINES:
p = Pipeline.objects.get(name=p_tuple[0])
p.pipeline_type = p_tuple[1]
p.save(update_fields=['pipeline_type'])
def revert_pipeline_types(apps, schema_editor):
Pipeline = apps.get_model('events', 'Pipeline')
for p_tuple in PIPELINES:
p = Pipeline.objects.get(name=p_tuple[0])
p.pipeline_type = DEFAULT_PIPELINE_TYPE
p.save(update_fields=['pipeline_type'])
class Migration(migrations.Migration):
dependencies = [
('events', '0038_pipeline_pipeline_type'),
]
operations = [
migrations.RunPython(update_pipeline_types, revert_pipeline_types),
]
......@@ -37,6 +37,9 @@ from cStringIO import StringIO
from hashlib import sha1
import shutil
from .managers import ProductionPipelineManager, ExternalPipelineManager
UserModel = get_user_model()
SERVER_TZ = pytz.timezone(settings.TIME_ZONE)
......@@ -65,14 +68,35 @@ class Group(models.Model):
def __unicode__(self):
return self.name
class Pipeline(models.Model):
PIPELINE_TYPE_EXTERNAL = 'E'
PIPELINE_TYPE_OTHER = 'O'
PIPELINE_TYPE_SEARCH_OTHER = 'SO'
PIPELINE_TYPE_SEARCH_PRODUCTION = 'SP'
PIPELINE_TYPE_CHOICES = (
(PIPELINE_TYPE_EXTERNAL, 'external'),
(PIPELINE_TYPE_OTHER, 'other'),
(PIPELINE_TYPE_SEARCH_OTHER, 'non-production search'),
(PIPELINE_TYPE_SEARCH_PRODUCTION, 'production search'),
)
name = models.CharField(max_length=100)
# Are submissions allowed for this pipeline?
enabled = models.BooleanField(default=True)
# XXX Need any additional fields? Like a librarian email? Or perhaps even fk?
# Pipeline type
pipeline_type = models.CharField(max_length=2,
choices=PIPELINE_TYPE_CHOICES)
# Add custom managers; must manually define 'objects' as well
objects = models.Manager()
production_objects = ProductionPipelineManager()
external_objects = ExternalPipelineManager()
class Meta:
permissions = (
('manage_pipeline', 'Can enable or disable pipeline'),
)
def __unicode__(self):
return self.name
......
......@@ -473,76 +473,6 @@ class TestEventNeighborsView(EventSetup, GraceDbTestBase):
self.assertEqual(response.status_code, 403)
class TestEventModifyT90(EventSetup, GraceDbTestBase):
@classmethod
def setUpTestData(cls):
super(TestEventModifyT90, cls).setUpTestData()
# Create a grb event
ext_group = Group.objects.create(name='External')
fermi_pipeline = Pipeline.objects.create(name='Fermi')
grb_search = Search.objects.create(name='GRB')
cls.grb_event = GrbEvent.objects.create(group=ext_group,
pipeline=fermi_pipeline, search=grb_search,
submitter=cls.internal_user)
ctype = ContentType.objects.get_for_model(GrbEvent)
perm = Permission.objects.create(codename='view_grbevent',
content_type=ctype)
assign_default_event_perms(cls.grb_event)
@classmethod
def setUpClass(cls):
super(TestEventModifyT90, cls).setUpClass()
cls.t90_data = {
'redshift': 2,
}
def test_basic_internal_user_t90(self):
"""Basic internal user can't t90 GRB events"""
url = reverse('modify_t90', args=[self.grb_event.graceid])
response = self.request_as_user(url, "POST", self.internal_user,
data=self.t90_data)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.content,
"You aren't authorized to modify GRB attributes.")
def test_privileged_internal_user_t90(self):
"""Privileged internal user can t90 GRB events"""
# Create permission and give to user
ctype = ContentType.objects.get_for_model(GrbEvent)
perm = Permission.objects.create(codename='t90_grbevent',
content_type=ctype)
perm.user_set.add(self.internal_user)
# Make request and check response
url = reverse('modify_t90', args=[self.grb_event.graceid])
response = self.request_as_user(url, "POST", self.internal_user,
data=self.t90_data)
# 302 response code means success since we were redirected to the
# event page
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('view',
args=[self.grb_event.graceid]))
self.grb_event.refresh_from_db()
self.assertEqual(self.grb_event.redshift, self.t90_data['redshift'])
def test_lvem_user_t90(self):
"""LV-EM user can't t90 GRB events"""
url = reverse('modify_t90', args=[self.grb_event.graceid])
response = self.request_as_user(url, "POST", self.lvem_user,
data=self.t90_data)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.templates[0].name, '403.html')
def test_public_user_t90(self):
"""Public user can't t90 GRB events"""
url = reverse('modify_t90', args=[self.grb_event.graceid])
response = self.request_as_user(url, "POST", data=self.t90_data)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.templates[0].name, '403.html')
class TestEventModifyPermissions(EventSetup, GraceDbTestBase):
@classmethod
......
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.urls import reverse
......@@ -52,6 +48,8 @@ def test_pipeline_change_views(view, standard_user, client):
# Create a pipeline
p, _ = Pipeline.objects.get_or_create(name='fake_pipeline')
p.pipeline_type = Pipeline.PIPELINE_TYPE_SEARCH_PRODUCTION
p.save(update_fields=['pipeline_type'])
# NOTE: get() is wired to post() in the view
response = client.get(reverse(view, args=[p.pk]))
......@@ -69,11 +67,10 @@ def test_pipeline_change_views_as_advocate(view, em_advocate_user, client):
# Create a pipeline
p, _ = Pipeline.objects.get_or_create(name='fake_pipeline')
p.pipeline_type = Pipeline.PIPELINE_TYPE_SEARCH_PRODUCTION
p.save(update_fields=['pipeline_type'])
# NOTE: get() is wired to post() in the view
with mock.patch('events.views.PIPELINE_LIST', new_callable=list) \
as mock_pipeline_list:
mock_pipeline_list.append(p.name)
response = client.get(reverse(view, args=[p.pk]))
response = client.get(reverse(view, args=[p.pk]))
assert response.status_code == 302
......@@ -26,9 +26,6 @@ urlpatterns = [
'(,(?P<delta2>[-+]?\d+)\)?)?/$'), views.neighbors, name="neighbors"),
# Form processing ---------------------------------------------------------
# Modify t90
url(r'^(?P<graceid>[GEHMT]\d+)/t90/$', views.modify_t90,
name="modify_t90"),
# Modify permissions
url(r'^(?P<graceid>[GEHMT]\d+)/perms/$', views.modify_permissions,
name="modify_permissions"),
......
......@@ -240,9 +240,9 @@ def delete_label(event, request, labelName, can_remove_protected=False,
# Next, check if the label is in the list of labels for the event. Throw out an
# error if it isn't. There might be a more elegant way of doing this.
if label not in event.labels.all():
d['warning'] = "No label '%s' associated with event %s" % (labelName, event.graceid)
raise ValueError("No label '%s' associated with event %s" % (labelName, event.graceid))
if not event.labelling_set.filter(label__name=labelName).exists():
d['warning'] = "No label '%s' associated with event %s" % (labelName, event.graceid)
raise Labelling.DoesNotExist("No label '%s' associated with event %s" % (labelName, event.graceid))
else:
this_label = Labelling.objects.get(
event = event,
......
......@@ -16,7 +16,7 @@ from core.file_utils import get_file_list
from core.http import check_and_serve_file
from .models import Event, Group, EventLog, Label, Tag, Pipeline, Search, GrbEvent
from .models import EMGroup, Signoff, PipelineLog
from .forms import CreateEventForm, SignoffForm
from .forms import CreateEventForm, SignoffForm, GrbEventUpdateForm
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import User, Permission
......@@ -369,8 +369,15 @@ def view(request, event):
# given permission to access an event. We want it to be the observers group.
context['lvem_group_name'] = settings.LVEM_OBSERVERS_GROUP
# GRB event update permissions/form
if event.pipeline.name in settings.GRB_PIPELINES:
context['can_modify_t90'] = request.user.has_perm('events.t90_grbevent')
context['can_update_grbevent'] = request.user.has_perm(
'events.t90_grbevent')
# If the user has permission, add the form
if context['can_update_grbevent']:
context['update_grbevent_form'] = \
GrbEventUpdateForm(instance=event)
# Is the user an external user? (I.e., not part of the LVC?) The template
# needs to know that in order to decide what pieces of information to show.
......@@ -747,41 +754,6 @@ def emobservation_entry(request, event, num=None):
else:
return HttpResponseBadRequest("This URL only supports POST.")
#
# Despite the name, this view function will handle updates to all of the
# hand-entered GRB data, including t90, redshift, and event designation.
#
@event_and_auth_required
def modify_t90(request, event):
if not request.method=='POST':
msg = 'This URL only allows POST.'
return HttpResponseBadRequest(msg)
if not isinstance(event, GrbEvent):
msg = 'This method only works on GrbEvent objects.'
return HttpResponseBadRequest(msg)
if not request.user.has_perm('events.t90_grbevent'):
msg = "You aren't authorized to modify GRB attributes."
return HttpResponseForbidden(msg)
designation = request.POST.get('designation', None)
redshift = request.POST.get('redshift', None)
t90 = request.POST.get('t90', None)
if not (t90 or designation or redshift):
msg = 'This method requires one of: designation, redshift, or t90 in POST.'
return HttpResponseBadRequest(msg)
if t90:
event.t90 = t90
elif redshift:
event.redshift = redshift
elif designation:
event.designation = designation
event.save()
# Finished. Redirect back to the event.
return HttpResponseRedirect(reverse("view", args=[event.graceid]))
def get_signoff_type(stype):
for t in Signoff.SIGNOFF_TYPE_CHOICES:
......@@ -975,7 +947,6 @@ def modify_signoff(request, event):
# Managing pipeline submissions -----------------------------------------------
PIPELINE_LIST = ['gstlal', 'pycbc', 'MBTAOnline', 'CWB', 'oLIB', 'spiir']
PIPELINE_LOG_ACTION_DICT = dict(PipelineLog.PIPELINE_LOG_ACTION_CHOICES)
@method_decorator(internal_user_required(raise_exception=True),
......@@ -986,8 +957,7 @@ class PipelineManageView(ListView):
log_number = 10
def get_queryset(self):
qs = Pipeline.objects.filter(name__in=PIPELINE_LIST).order_by('name')
return qs
return Pipeline.production_objects.order_by('name')
def get_context_data(self, **kwargs):
context = super(PipelineManageView, self).get_context_data(**kwargs)
......@@ -1033,8 +1003,7 @@ class PipelineEnableView(UpdateView):
success_url = reverse_lazy('manage-pipelines')
def get_queryset(self):
qs = Pipeline.objects.filter(name__in=PIPELINE_LIST)
return qs
return Pipeline.production_objects.all()
def get(self, request, *args, **kwargs):
return self.post(request, *args, **kwargs)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-08 20:30
from __future__ import unicode_literals
from django.db import migrations
# NOTE: membership in the 'executives' group has been managed by a script in a
# separate repo in the past, so this migration is the starting point for
# going away from that workflow. Also, the executives group will eventually
# be replaced by the 'access_managers' group, but until we update the
# permissions structure in the events app, we have to maintain it.
EXECUTIVES = [
'patrick.brady@LIGO.ORG',
'tanner.prestegard@LIGO.ORG',
'alexander.pace@LIGO.ORG',
]
def update_membership(apps, schema_editor):
User = apps.get_model('auth', 'User')
AuthGroup = apps.get_model('ligoauth', 'AuthGroup')
# Get executives group
execs = AuthGroup.objects.get(name='executives')
# Clear membership
execs.user_set.clear()
# Add users
for username in EXECUTIVES:
user, _ = User.objects.get_or_create(username=username)
execs.user_set.add(user)
class Migration(migrations.Migration):
dependencies = [
('ligoauth', '0047_add_emfollow_cert'),
]
operations = [
migrations.RunPython(update_membership, migrations.RunPython.noop),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-08 20:37
from __future__ import unicode_literals
from django.db import migrations
ACCESS_MANAGERS = [
'alexander.pace@LIGO.ORG',
'brian.oreilly@LIGO.ORG',
'emfollow',
'erik.katsavounidis@LIGO.ORG'
'keita.kawabe@LIGO.ORG',
'leo.singer@LIGO.ORG',
'patrick.brady@LIGO.ORG',
'peter.shawhan@LIGO.ORG',
'sarah.antier@LIGO.ORG',
'shaon.ghosh@LIGO.ORG',
'tanner.prestegard@LIGO.ORG',
]
def update_membership(apps, schema_editor):
User = apps.get_model('auth', 'User')
AuthGroup = apps.get_model('ligoauth', 'AuthGroup')
# Get access_managers group
group = AuthGroup.objects.get(name='access_managers')
# Clear membership
group.user_set.clear()
# Add users
for username in ACCESS_MANAGERS:
user, _ = User.objects.get_or_create(username=username)
group.user_set.add(user)
class Migration(migrations.Migration):
dependencies = [
('ligoauth', '0048_update_executives_membership'),
]
operations = [
migrations.RunPython(update_membership, migrations.RunPython.noop),
]
......@@ -37,6 +37,12 @@ def parse_superevent_id(name, toks, filter_prefix=None):
if (toks.prefix == Superevent.GW_ID_PREFIX):
toks.suffix = toks.suffix.upper()
# Allow flexible suffix capitalization
if (toks.prefix == Superevent.GW_ID_PREFIX):
toks.suffix = toks.suffix.upper()
else:
toks.suffix = toks.suffix.lower()
# Combine into full ID and get lookup kwargs
s_id = toks.preprefix + toks.prefix + toks.date + toks.suffix
f_kwargs = Superevent.get_filter_kwargs_for_date_id_lookup(s_id)
......
......@@ -43,6 +43,10 @@ SUPEREVENT_QUERY_TEST_DATA = [
("", DEFAULT_Q),
("id: S190509bc",
Q(**Superevent.get_filter_kwargs_for_date_id_lookup("S190509bc"))),
("id: Tgw190331eBz",
Q(**Superevent.get_filter_kwargs_for_date_id_lookup("TGW190331EBZ"))),
("id: ms190331BCdE",
Q(**Superevent.get_filter_kwargs_for_date_id_lookup("MS190331bcde"))),
("superevent_id: S190509bc",
Q(**Superevent.get_filter_kwargs_for_date_id_lookup("S190509bc"))),
("S190509bc",
......
......@@ -86,12 +86,17 @@ def populate_values(voevent, event_or_superevent):
# Parse parameters
for parameter in PARAMETER_MAPPINGS:
result = None
for path in PARAMETER_MAPPINGS[parameter]:
result = None
try:
result = root.find(path)
except SyntaxError as e:
pass
else:
# If result is not None, we've found something, so let's break
if result is not None:
break
if result is None:
continue
......@@ -114,9 +119,10 @@ def populate_values(voevent, event_or_superevent):
for parameter, str_list in DESCRIPTION_PARAMETER_MAPPINGS.items():
for s in str_list:
if any([s in dp for dp in desc_text]):
setattr(voevent, parameter, True)
value = True
else:
setattr(voevent, parameter, False)
value = False
setattr(voevent, parameter, value)
# Save VOEvent
voevent.save()
......
......@@ -12,6 +12,7 @@
<script src="{% static "moment/moment.js" %}"></script>
<script src="{% static "moment-timezone/builds/moment-timezone-with-data.min.js" %}"></script>
<script src="{% static "dojo/dojo.js" %}" data-dojo-config="async: true"></script>
<script src="{% static "jquery/dist/jquery.min.js" %}"></script>
<!-- Styles for dgrid -->
<!-- <link rel="stylesheet" href="{% static "dgrid/css/dgrid.css" %}" /> -->
<!-- Styles for the editor components -->
......
......@@ -90,19 +90,18 @@
</tbody>
</table>
{% if can_modify_t90 %}
{% if can_update_grbevent %}
<div class="content-area">
<form action="{% url "modify_t90" object.graceid %}" method="post">
<input type="text" name="t90" value="{{ object.t90 }}">
<input type="submit" value="Update T90" class="t90ButtonClass">
</form>
<form action="{% url "modify_t90" object.graceid %}" method="post">
<input type="text" name="redshift" value="{{ object.redshift }}">
<input type="submit" value="Update redshift" class="t90ButtonClass">
</form>
<form action="{% url "modify_t90" object.graceid %}" method="post">
<input type="text" name="designation" value="{{ object.designation }}">
<input type="submit" value="Update designation" class="t90ButtonClass">
<h3>Update GRB Event</h3>
<form id="update_grbevent_form" action="{% url "legacy_apiweb:default:events:update-grbevent" object.graceid %}">
<table>
{{ update_grbevent_form.as_table }}
<tr>
<td></td>
<td><input type="submit" value="Update" disabled></td>
</tr>
</table>
</form>
</div>
{% endif %}
......
......@@ -225,6 +225,49 @@ require([
Save, Preview, ScrollPane, Uploader) {
parser.parse();
// We don't enable the input buttons until right now otherwise fast users
// can trigger the form before the javascript is ready... not ideal
$("#update_grbevent_form input[type=submit]").attr('disabled', false);
// Update GRB form
$("#update_grbevent_form").submit(function(e) {
e.preventDefault();
// Get button and disable it to prevent multiple clicks
var submit_button = $(this).find("input[type=submit]");
submit_button.attr("disabled", true);
// Make ajax request - we have to specify a PATCH method here
// since we can't do it in the HTML
$.ajax({
type: 'PATCH',
url: $(this).attr('action'),
data: $(this).serialize(),
success: function(resp) {
// Don't need to re-enable since we reload the page
//submit_button.attr("disabled", false);
location.reload(true);
},
error: function(error) {
//this.button.set("disabled", false);
var err_msg = "Error " + error.status + ": ";
if (error.responseText != "") {
err_msg += error.responseText;
} else {
err_msg += error.statusText;
}
if (error.status == 404) {
err_msg += ". Reload the page.";
}
alert(err_msg);
// Re-enable submit button
submit_button.attr("disabled", false);
}
});
});
//----------------------------------------------------------------------------------------
// Some utility functions
//----------------------------------------------------------------------------------------
......
......@@ -76,11 +76,6 @@ body {
</h2>
<div class="text">
{# Can customize main informational display on home page or just use default #}
{% if information %}
{{ information|safe }}
{% else %}
<p>
The gravitational-wave candidate event database (GraceDB) is a service operated by the <a href="https://www.ligo.org/">LIGO Scientific Collaboration</a>. It provides a centralized location for aggregating and retrieving information about candidate gravitational-wave events. GraceDB provides an <a href="{% url "legacy_apiweb:default:root" %}">API</a> for programmatic access, and a <a href="https://ligo-gracedb.readthedocs.io/">client package</a> is available for interacting with the API.
</p>
......@@ -90,6 +85,8 @@ body {
<li>Information about GW alerts and real-time data products is available in the <a href="https://emfollow.docs.ligo.org/userguide/">LIGO/Virgo Public Alert Guide</a>.</li>
<li>Found a bug? LIGO/Virgo users can report issues on the GraceDB <a href="https://git.ligo.org/lscsoft/gracedb/issues">Gitlab page</a>.</li>
</ul>
{% if information %}
{{ information|safe }}
{% endif %}
<p><b>Server code version: <a href="https://git.ligo.org/lscsoft/gracedb/tree/gracedb-{{server_version}}">{{server_version}}</a></b></p>
</div>
......
......@@ -7,7 +7,7 @@
{% load static %}
{% block headcontents %}
<link rel="stylesheet" href="{% static "css/bootstrap_buttons.css" %}"></script>
<link rel="stylesheet" href="{% static "css/bootstrap_buttons.css" %}">
{{ block.super }}
{% endblock %}
......
......@@ -3,6 +3,7 @@ Django==1.11.23
django-debug-toolbar==1.9.1
django-extensions==2.0.5
django-guardian==1.4.9
django-mock-queries==2.1.3
django-model-utils==3.1.1
django-ses
django-silk==3.0.1
......@@ -41,6 +42,8 @@ pyparsing==2.3.0
pytest-cov==2.6.1
pytest-django==3.4.8
pytz==2018.9
# Attempt to install VOEventLib via pip
git+https://git.ligo.org/tanner.prestegard/VOEventLib.git@e5e5766701d24db453207992635c14143129ebd7#egg=VOEventLib
# Fixed at old versions to prevent bugs:
# https://github.com/fritzy/SleekXMPP/issues/477
# https://github.com/etingof/pyasn1/issues/112
......