...
 
Commits (8)
......@@ -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 \
......
......@@ -50,8 +50,8 @@ master_doc = 'index'
# General information about the project.
project = u'GraceDB'
copyright = u'2015, Brian Moe, Branson Stephens, Patrick Brady'
author = u'Brian Moe, Branson Stephens, Patrick Brady'
copyright = u'2019, Tanner Prestegard, Alexander Pace, Brian Moe, Branson Stephens, Patrick Brady'
author = u'Tanner Prestegard, Alexander Pace, Brian Moe, Branson Stephens, Patrick Brady'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
......
......@@ -35,7 +35,6 @@ purposes. The diagram above depicts a typical sequence of events:
Overview of components
======================
GraceDB consists of the server (`gracedb.ligo.org <https://gracedb.ligo.org>`__)
and a set of client tools. Two user interfaces are available: the web interface
for browser access (i.e., the one you are using now), and the
......@@ -44,20 +43,18 @@ These interfaces represent the information in GraceDB in different ways:
the web interface naturally represents information as HTML pages, whereas
the REST interface delivers JSON-serialized data.
The client tools (available via ``pip``, SL6 or Debian packages, and source
build, see :ref:`installing_the_client`) provide a way to interact via the REST API.
These tools include a Python client class
with methods for all common GraceDB operations.
There is also a ``gracedb`` executable for the command line with much of
the same functionality.
The `ligo-gracedb client package <https://gw.readthedocs.io/ligo-gracedb>`__ provides a convenient way to interact with the REST API.
This package includes a Python client class with methods for all common GraceDB operations.
There is also a ``gracedb`` executable for the command line with much of the same functionality.
The GraceDB API conforms to the RESTful principles of "uniform interface" and
"resource-oriented architecture".
Where can I go for help?
==================================
This documentation is not as great as it could be, but we are working on it.
This documentation is not as great as it could be, but
we are working on it. For help with issues not addressed here, please
send mail to uwm-help@ligo.org.
LIGO/Virgo users can join the GraceDB channel in the collaboration's Mattermost instance or email the DASWG mailing list for help.
To report a problem, either `post an issue <https://git.ligo.org/lscsoft/gracedb/issues>`__ or send mail to uwm-help@ligo.org.
......@@ -11,8 +11,9 @@ Contents:
.. toctree::
:maxdepth: 2
ref_manual
ref_manual
tutorials
Documentation for the ligo-gracedb client package <https://gw.readthedocs.io/ligo-gracedb>
LIGO/Virgo Public Alert Guide <https://emfollow.docs.ligo.org/userguide/>
Report a bug (LIGO/Virgo users) <https://git.ligo.org/lscsoft/gracedb/issues>
......
=====================================
Integration with LVAlert
=====================================
======================================
LVAlert notifications (LVC users only)
======================================
Introduction
===============================================
============
LVAlert (LIGO-Virgo Alert system) is an XMPP-based messaging platform used within the LVC.
This document will describe how GraceDB uses LVAlert, which nodes it manages and publishes to, and the content of LVAlert messages sent by GraceDB.
.. GraceDB uses `LVAlert <https://wiki.ligo.org/Computing/DASWG/LVAlert>`__ to send alerts to listeners within the LVC when.
GraceDB uses `LVAlert <https://wiki.ligo.org/Computing/DASWG/LVAlert>`__ to send alerts to listeners within the LVC.
The content of the LVAlert message is designed to convey actionable information about a state change in GraceDB, whether it involves the creation of a new event, or the updating or labeling of an existing one.
Some helpful resources for installing and configuring LVAlert are:
- Main ligo-lvalert `documentation <https://lscsoft.docs.ligo.org/lvalert/index.html>`__
- ligo-lvalert `user guide <https://lscsoft.docs.ligo.org/lvalert/guide.html>`__
- :ref:`Tutorial<responding_to_lvalert>` on setting up LVAlert and configuring your listener.
LVAlert and GraceDB
===================
Generally speaking, GraceDB uses LVAlert to send "push" notifications about different actions that may be taken on the service.
Users can subscribe to different nodes (more below) to receive these notifications, filter their content, and optionally trigger follow-up processes, like data quality checks, parameter estimation, and more.
The content of an LVAlert message is designed to convey actionable information about a state change in GraceDB, including event creation, annotation, and other actions.
.. NOTE::
An LVAlert message is sent out for *any* new event or annotation that arrives in the GraceDB database.
This means that message volumes may be very high under certain circumstances, and appropriate filtering is required in order for LVAlert to be useful.
This means that message volumes may be very high under certain circumstances, and listeners should be constructed to appropriately filter the messages.
Listening to specific event streams
==============================================
LVAlert nodes managed by GraceDB
================================
By running ``lvalert_listen``, you will receive messages over all **nodes** to which you are subscribed.
There are two types of nodes to which GraceDB broadcasts alerts: event nodes and superevent nodes.
Event nodes
-----------
Event node names consist of at least two elements::
<group_name>_<pipeline_name>
......@@ -30,46 +46,45 @@ One can also specify the search name::
which has the effect of narrowing down the messages to only those related to a specific search.
For example, the node ``burst_cwb_allsky`` will contain messages relating to the AllSky search, but not the MDC search.
Note that GraceDB tries to send a message to all applicable nodes.
Thus, a message sent to the node ``burst_cwb_allsky`` will *also* be sent to the more generic node ``burst_cwb``.
This property allows the user to filter according to search by specifying different LVAlert processing scripts for different nodes.
It is important to note that GraceDB will send an LVAlert to all nodes which match the parameters of the event in question.
For example, the creation of a Burst-cWB-AllSky event will result in messages being sent to the ``burst_cwb_allsky`` node, as well as the more generic ``burst_cwb`` node.
This feature allows the user to filter according to search by specifying different LVAlert processing scripts for different nodes.
Superevent nodes
----------------
There are only three superevent nodes; one for each category of superevent:
- ``superevent``
- ``test_superevent``
- ``mdc_superevent``
To see the names of all available nodes, simply execute::
lvalert_admin -a username -i
For more information on how to receive and react to LVAlert messages, see :ref:`responding_to_lvalert`.
Most users will be interested in the ``superevent`` node in order to receive LVAlerts about real GW candidates.
LVAlert message contents
================================================
GraceDB sends messages as a JSON-encoded dictionary.
Contents of LVAlerts sent by GraceDB
====================================
GraceDB sends LVAlert messages as a JSON-encoded dictionary.
This dictionary contains the following keys:
- ``alert_type``: short string representing the. Examples: ``new``, ``update``, ``label_added``, etc. All alert types are shown in the tables below.
- ``alert_type``: short string representing the action which triggered the alert. Examples: ``new``, ``update``, ``label_added``, etc. All alert types are shown in the tables below.
- ``data``: a dictionary representing the relevant object (label, log message, etc.)
- ``object``: a dictionary representing the corresponding "parent" object
- ``object``: a dictionary representing the corresponding "parent" object (i.e., the event or superevent which a log, label, etc. is attached to).
- ``uid``: the unique ID of the relevant event or superevent
Below, we describe the alert contents in more detail.
Examples of the various ``data``/``object`` dictionaries are available in :ref:`models`.
See :ref:`below<example_permissions_list>` for one additional example (list of permissions).
Event alerts
------------
For alerts related to events, the following things are always true:
- ``uid`` is always the event's ``graceid`` (example: G123456).
- ``object`` is always a dictionary corresponding to the event which is affected by the label, log, VOEvent, etc.
The following table shows the ``alert_type`` and ``data`` for different actions:
+-------------------------+---------------------------------+---------------------------------------------------------+
......@@ -113,7 +128,6 @@ The following table shows the ``alert_type`` and ``data`` for different actions:
Superevent alerts
-----------------
For alerts related to superevents, the following things are always true:
- ``uid`` is always the superevent's ``superevent_id`` (example: S800106D).
......@@ -162,14 +176,5 @@ The following table shows the ``alert_type`` and ``data`` for different actions:
Example: list of permission dictionaries
----------------------------------------
.. literalinclude:: dicts/permissions.list
:language: JSON
Further reading on LVAlert
=====================================================
Further information on using LVAlert can be found on the
`LVAlert Project Page <https://wiki.ligo.org/Computing/DASWG/LVAlert>`__
and the `LVAlert Howto <https://wiki.ligo.org/Computing/DASWG/LVAlertHowto>`__.
......@@ -2,6 +2,15 @@
Features for EM Collaboration
=============================
.. NOTE::
This document describes access and features provided to members of
collaborations which the LSC had an MOU with in O1 and O2. This access
is maintained at present (August 2019), but may not be going forward.
The information contained on this page will not be updated or
maintained, and will eventually be removed.
On logging in
=============
......@@ -22,6 +31,7 @@ according to the identity you used for registering for LV-EM membership at
there is no way (at present) to map these different identities to the same
underlying user.
.. _basic_auth_for_lvem:
Scripted access for LV-EM members
......@@ -65,14 +75,12 @@ to make sure that only you can read it). The ``.netrc`` file could look like thi
Place the resulting ``.netrc`` file in your home directory.
Once that's done, you should be able to access the GraceDB REST API
using any tool that supports basic auth.
For example, you can use the GraceDB Python client in much the same
way as described in :ref:`rest_client_basic_usage`, except that the
client class is specially formulated for basic auth::
For example, you can use the GraceDB Python client::
from ligo.gracedb.rest import GraceDbBasic, HTTPError
from ligo.gracedb.rest import GraceDb, HTTPError
service_url = 'https://gracedb.ligo.org/api/'
client = GraceDbBasic(service_url)
client = GraceDb(service_url, username='user', password='pass')
try:
r = client.ping()
......@@ -82,16 +90,14 @@ client class is specially formulated for basic auth::
print "Response code: %d" % r.status
print "Response content: %s" % r.json()
The only real difference is that the ``GraceDbBasic`` client class is used instead
of the ``GraceDb`` class (which assumes that X509 credentials are available).
If you're not comfortable using Python for scripted access to GraceDB, it is
also possible to use ``curl`` to directly make requests to the server with the
same basic auth credentials. Some examples of using curl are available
`here <https://gw-astronomy.org/wiki/LV_EM/TechInfo>`__.
Downloading a skymap
======================
The GraceDB Python client can be used to download
files from Gracedb or add comments, plots, or observation records (see
the next section). Here, we'll
......@@ -117,11 +123,11 @@ This file can be retrieved in the following way::
out_file.write(r.read())
out_file.close()
.. _create_emobservation:
Reporting coordinates of followup observations
===============================================
In the following example, the GraceDB Python client is used to create an
observation record consisting of three separate footprints::
......@@ -169,7 +175,5 @@ something like "delete GraceDB EMObservation" in the subject line. Tell us
which entry you'd like deleted, and we'll take care of it. In the future, we
are hoping to make these observation records editable by the submitter.
For more on the GraceDB event page and creating EM observation records, see
`this <https://www.youtube.com/watch?v=oIJE4dTISs4>`__ helpful video
by Roy Williams. There is a companion video on the SkymapViewer
`here <https://www.youtube.com/watch?v=ydXUD9KIN98>`__.
.. For more on the GraceDB event page and creating EM observation records, see `this <https://www.youtube.com/watch?v=oIJE4dTISs4>`__ helpful video by Roy Williams.
.. There is a companion video on the SkymapViewer `here <https://www.youtube.com/watch?v=ydXUD9KIN98>`__.
......@@ -12,10 +12,10 @@ Contents:
models
web
rest
auth
queries
labels
lvalert
notifications
lvem
auth
......@@ -6,6 +6,14 @@ Responding to LVAlert Messages
.. sectionauthor:: Reed Essick
.. NOTE::
This tutorial may not be fully up-to-date. The preferred resource for
installing ligo-lvalert and configuring a listener is the
ligo-lvalert `user guide <https://lscsoft.docs.ligo.org/lvalert/guide.html>`__.
However, at present (August 2019), it is not fully completed, and the tutorial
on this page may still provide some useful information.
This tutorial will show you how to
* register to receive LVAlerts
* subscribe and unsubscribe from pubsub nodes
......
This diff is collapsed.
......@@ -44,26 +44,11 @@ The existing tags are also shown in the same column as the message itself, as is
Users are free to create new tags for their own purposes (e.g., searching through annotations at some
later date), but only a pre-determined list of tags is used to create title pane sections.
For more on the GraceDB event page, see `this <https://www.youtube.com/watch?v=oIJE4dTISs4>`_ helpful video by Roy Williams, which is geared toward LV-EM users.
There also is a `companion video <https://www.youtube.com/watch?v=ydXUD9KIN98>`__ on the SkymapViewer.
.. For more on the GraceDB event page, see `this <https://www.youtube.com/watch?v=oIJE4dTISs4>`_ helpful video by Roy Williams, which is geared toward LV-EM users.
.. There also is a `companion video <https://www.youtube.com/watch?v=ydXUD9KIN98>`__ on the SkymapViewer.
Understanding the superevent detail page
========================================
The detail page for a superevent can be accessed similarly to an event page.
The content is analogous to that shown on the event page, although it contains information about the superevent in general, as well as a table summarizing the information about the superevent's preferred event.
Signing up for email or phone alerts (LVC only)
=======================================================
LVC users may set up email or phone notifications for events that come from specific pipelines and have specific labels.
This feature is available to LVC users only because the events are not vetted before the alert is sent out.
For non-LVC users, GCN will provide the equivalent functionality.
See the LV-EM `techinfo page <https://gw-astronomy.org/wiki/LV_EM/TechInfo>`__.)
In order to sign up for an alert, you must first create a contact by clicking on "OPTIONS" in the navigation menu, and then "Create New Contact."
Follow the instructions on that page to add your contact information.
Next, return to the options page and click "Create New Notification".
This page allows you to set the criteria for alerts to be sent to the contact that you created in the previous step.
These are currently only available for events, but may be extended to superevents in the future.
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, Labelling, 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
......
......@@ -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']
......@@ -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()
......
......@@ -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
......
......@@ -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"),
......
......@@ -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:
......
......@@ -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
//----------------------------------------------------------------------------------------
......
......@@ -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
......