...
 
Commits (41)
......@@ -8,8 +8,14 @@ Run ./check_shibboleth_status -h for help.
'''
# Imports
import argparse, urllib2, sys
import argparse
import sys
import xml.etree.ElementTree as ET
try:
from urllib.request import urlopen
from urllib.error import URLError
except ImportError: # python < 3
from urllib2 import (urlopen, URLError)
# Parameters - may need to be modified in the future
# if Shibboleth status pages change or new metadata
......@@ -48,12 +54,12 @@ metadata_feeds = args.feeds.split(",")
# Get XML data from URL.
host_url = host + "/" + urlpath
try:
response = urllib2.urlopen(host_url, timeout=timeout)
except urllib2.URLError:
print "Error opening Shibboleth status page (" + host_url + ")."
response = urlopen(host_url, timeout=timeout)
except URLError:
print("Error opening Shibboleth status page (" + host_url + ").")
sys.exit(2)
except:
print "Unknown error opening Shibboleth status page (" + host_url + ")."
print("Unknown error opening Shibboleth status page (" + host_url + ").")
sys.exit(3)
# Convert from string to ElementTree
......@@ -61,11 +67,11 @@ try:
status_tree = ET.fromstring(response.read())
except ET.ParseError:
# Error parsing response.
print "Error parsing response from server - not in XML format."
print("Error parsing response from server - not in XML format.")
sys.exit(2)
except:
# Error that is not ParseError.
print "Unknown error occurred when parsing response from server."
print("Unknown error occurred when parsing response from server.")
sys.exit(3)
response.close()
......@@ -75,12 +81,12 @@ response.close()
for tag in tags_to_check:
status_tag = status_tree.find(tag)
if (status_tag is None):
print "Error: tag \'" + tag + "\' not found."
print("Error: tag \'" + tag + "\' not found.")
sys.exit(2)
else:
status_OK = status_tag.find('OK')
if (status_OK is None):
print "Error: tag \'" + tag + "\' is not OK."
print("Error: tag \'" + tag + "\' is not OK.")
sys.exit(2)
# Check 2: make sure metadata feeds that we expect
......@@ -90,12 +96,12 @@ srcs = [element.attrib['source'] for element in metaprov_tags]
for feed in metadata_feeds:
feed_found = [src.lower().find(feed) >= 0 for src in srcs]
if (sum(feed_found) < 1):
print "MetadataProvider " + feed + " not found."
print("MetadataProvider " + feed + " not found.")
sys.exit(2)
elif (sum(feed_found) < 1):
print "MetadataProvider " + feed + "found in multiple elements."
print("MetadataProvider " + feed + "found in multiple elements.")
sys.exit(2)
# If we make it to this point, everything is OK.
print "All MetadataProviders found. Status and SessionCache are OK."
print("All MetadataProviders found. Status and SessionCache are OK.")
sys.exit(0)
......@@ -114,7 +114,7 @@ example, here is how to grant ``view`` permissions to ``public``::
group_name = 'public'
perm_shortname = 'view'
url = g.service_url + urllib.quote('events/%s/%s/%s' % (graceid, group_name, perm_codename))
url = g.service_url + urllib.parse.quote('events/%s/%s/%s' % (graceid, group_name, perm_codename))
r = g.put(url)
Templates
......
......@@ -62,7 +62,7 @@ in real life (from inside the Django shell)::
>>> u = User.objects.get(username='albert.einstein@LIGO.ORG')
>>> if p in u.user_permissions.all():
...: print "Albert can add events!"
...: print("Albert can add events!")
The Django ``User`` class has a convenience function ``has_perm`` to
make this easier::
......@@ -72,7 +72,7 @@ make this easier::
>>> u = User.objects.get(username='albert.einstein@LIGO.ORG')
>>> if u.has_perm('events.add_event'):
...: print "Albert can add events!"
...: print("Albert can add events!")
Again, notice that the ``has_perm`` function needs the codename to be scoped by
the app to which the model belongs. Both are required to fully specify the model.
......@@ -124,7 +124,7 @@ event data. Thus, we have added a custom ``view`` permission for the event model
>>> perms = Permission.objects.filter(codename__startswith='view')
>>> for p in perms:
...: print p.codename
...: print(p.codename)
...:
view_coincinspiralevent
view_event
......@@ -314,7 +314,7 @@ can be done by adding the permission by hand::
>>> u = User.objects.get(username='albert.einstein@LIGO.ORG')
>>> u.user_permissions.add(p):
...: print "Albert can add events!"
...: print("Albert can add events!")
Granting permission to populate a pipeline
------------------------------------------
......
......@@ -75,9 +75,9 @@ Shibbolized client as follows::
try:
r = client.ping()
except HTTPError, e:
print e.message
except HTTPError as e:
print(e.message)
print "Response code: %d" % r.status
print "Response content: %s" % r.json()
print("Response code: %d" % r.status)
print("Response content: %s" % r.json())
......@@ -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,33 +75,29 @@ 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()
except HTTPError, e:
print e.message
print "Response code: %d" % r.status
print "Response content: %s" % r.json()
except HTTPError as e:
print(e.message)
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::
......@@ -147,7 +153,7 @@ observation record consisting of three separate footprints::
decList, decWidthList, startTimeList, durationList, comment)
if r.status == 201: # 201 means 'Created'
print 'Success!'
print('Success!')
Note that the start times are always assumed to be in UTC. For users not
familiar with Python, there are several other options available for uploading
......@@ -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
......@@ -321,7 +329,7 @@ Create a Python executable ``iReact.py`` and fill it with the following::
import sys
alert = json.loads(sys.stdin.read())
print 'uid : ' + alert['uid']
print('uid : ' + alert['uid'])
Don't forget to give this executable permissions with::
......@@ -369,7 +377,7 @@ Open ``iReact.py`` and modify it so it reads::
from ligo.gracedb.rest import GraceDb
alert = json.loads(sys.stdin.read())
print 'uid : ' + alert['uid']
print('uid : ' + alert['uid'])
gdb = GraceDb() ### instantiate a GraceDB object which connects to the default server
......@@ -411,15 +419,14 @@ more example for what ``iReact.py`` might look like::
FarThr = float(sys.argv[1])
alert = json.loads(sys.stdin.read())
print 'uid : '+alert['uid']
print('uid : '+alert['uid'])
gdb = GraceDb() ### instantiate a GraceDB object which connects to the default server
if alert['alert_type'] == 'new': ### the event was just created and this is the first announcment
if alert['far'] < FarThr:
file_obj = open("iReact.txt", "w")
print >> file_obj, "wow! this was a rare event! It had FAR = %.3e < %.3e, which was my threshold"%(alert['far'], FarThr)
file_obj.close()
with open("iReact.txt", "w") as file_obj:
print("wow! this was a rare event! It had FAR = %.3e < %.3e, which was my threshold"%(alert['far'], FarThr), file=file_obj)
gdb.writeLog( alert['uid'], message="user.name heard an alert about this new event!", filename="iReact.txt", tagname=["data_quality"] )
Try to figure out exactly what this version does. If you can
......
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.
......@@ -8,7 +8,8 @@ from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.core.mail import EmailMessage
from django.db import models
from django.utils import timezone
from django.utils import six, timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.http import urlencode
from django_twilio.client import twilio_client
......@@ -55,7 +56,6 @@ class Contact(CleanSaveModel):
updated = models.DateTimeField(auto_now=True)
verified_time = models.DateTimeField(null=True, blank=True, editable=False)
def __str__(self):
return "{0}: {1}".format(self.user.username, self.description)
......@@ -168,6 +168,7 @@ class Contact(CleanSaveModel):
###############################################################################
# Notifications ###############################################################
###############################################################################
@python_2_unicode_compatible
class Notification(models.Model):
# Notification categories
NOTIFICATION_CATEGORY_EVENT = 'E'
......@@ -196,10 +197,12 @@ class Notification(models.Model):
pipelines = models.ManyToManyField('events.pipeline', blank=True)
searches = models.ManyToManyField('events.search', blank=True)
def __unicode__(self):
return (u"%s: %s") % (
self.user.username,
self.display()
def __str__(self):
return six.text_type(
"{username}: {display}".format(
username=self.user.username,
display=self.display()
)
)
def display(self):
......
try:
from functools import reduce
except ImportError: # python < 3
pass
from django.conf import settings
from django.db.models import Q
......
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.contrib.auth.models import Group as AuthGroup
......
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.test import override_settings
......
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.test import override_settings
......
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.test import override_settings
......
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
......
......@@ -65,8 +65,7 @@ class CreateNotificationView(MultipleFormView):
return kw
def form_valid(self, form):
if form.cleaned_data.has_key('key_field'):
form.cleaned_data.pop('key_field')
form.cleaned_data.pop('key_field', None)
# Add user (from request) and category (stored on form class) to
# the form instance, then save
......@@ -169,8 +168,7 @@ class CreateContactView(MultipleFormView):
def form_valid(self, form):
# Remove key_field, add user, and save form
if form.cleaned_data.has_key('key_field'):
form.cleaned_data.pop('key_field')
form.cleaned_data.pop('key_field', None)
form.instance.user = self.request.user
form.save()
......
......@@ -13,7 +13,8 @@ def gracedb_exception_handler(exc, context):
if hasattr(exc, 'detail') and hasattr(exc.detail, 'values'):
# Combine values into one list
exc_out = [item for sublist in exc.detail.values() for item in sublist]
exc_out = [item for sublist in list(exc.detail.values())
for item in sublist]
# For only one exception, just print it rather than the list
if len(exc_out) == 1:
......
from base64 import b64encode
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.urls import reverse
......@@ -31,9 +34,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can authenticate to API with correct password"""
# Set up and make request
url = api_reverse('api:root')
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
headers = {
'HTTP_AUTHORIZATION': 'Basic {0}'.format(user_and_pass),
}
......@@ -53,16 +59,20 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can't authenticate with wrong password"""
# Set up and make request
url = api_reverse('api:root')
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password='b4d')).decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password='b4d'
).encode()
).decode("ascii")
headers = {
'HTTP_AUTHORIZATION': 'Basic {0}'.format(user_and_pass),
}
response = self.client.get(url, data=None, **headers)
# Check response
self.assertEqual(response.status_code, 403)
self.assertIn('Invalid username/password', response.content)
self.assertContains(response, 'Invalid username/password',
status_code=403)
def test_user_authenticate_to_api_with_expired_password(self):
"""User can't authenticate with expired password"""
......@@ -73,17 +83,20 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
# Set up and make request
url = api_reverse('api:root')
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
headers = {
'HTTP_AUTHORIZATION': 'Basic {0}'.format(user_and_pass),
}
response = self.client.get(url, data=None, **headers)
# Check response
self.assertEqual(response.status_code, 403)
self.assertIn('Your password has expired', response.content)
self.assertContains(response, 'Your password has expired',
status_code=403)
class TestGraceDbX509Authentication(GraceDbApiTestBase):
......@@ -139,8 +152,8 @@ class TestGraceDbX509Authentication(GraceDbApiTestBase):
response = self.client.get(url, data=None, **headers)
# Check response
self.assertEqual(response.status_code, 401)
self.assertIn("Invalid certificate subject", response.content)
self.assertContains(response, 'Invalid certificate subject',
status_code=401)
def test_inactive_user_authenticate(self):
"""Inactive user can't authenticate"""
......@@ -156,8 +169,8 @@ class TestGraceDbX509Authentication(GraceDbApiTestBase):
response = self.client.get(url, data=None, **headers)
# Check response
self.assertEqual(response.status_code, 401)
self.assertIn("User inactive or deleted", response.content)
self.assertContains(response, 'User inactive or deleted',
status_code=401)
def test_authenticate_cert_with_proxy(self):
"""User can authenticate to API with proxied X509 certificate"""
......
......@@ -45,9 +45,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can authenticate to API with correct password"""
# Set up request
request = self.factory.get(api_reverse('api:root'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Authentication attempt
......@@ -60,8 +63,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can't authenticate with wrong password"""
# Set up request
request = self.factory.get(api_reverse('api:root'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password='b4d')).decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password='b4d'
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Authentication attempt should fail
......@@ -77,9 +84,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
# Set up request
request = self.factory.get(api_reverse('api:root'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Authentication attempt should fail
......@@ -91,9 +101,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
"""User can't authenticate to a non-API URL path"""
# Set up request
request = self.factory.get(reverse('home'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Try to authenticate
......@@ -108,9 +121,12 @@ class TestGraceDbBasicAuthentication(GraceDbApiTestBase):
# Set up request
request = self.factory.get(api_reverse('api:root'))
user_and_pass = b64encode(b"{username}:{password}".format(
username=self.lvem_user.username, password=self.password)) \
.decode("ascii")
user_and_pass = b64encode(
"{username}:{password}".format(
username=self.lvem_user.username,
password=self.password
).encode()
).decode("ascii")
request.META['HTTP_AUTHORIZATION'] = 'Basic {0}'.format(user_and_pass)
# Authentication attempt should fail
......
import mock
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.core.cache import caches
......@@ -24,5 +27,4 @@ class TestThrottling(GraceDbApiTestBase):
# Second response should get throttled
response = self.request_as_user(url, "GET")
self.assertEqual(response.status_code, 429)
self.assertIn('Request was throttled', response.content)
self.assertContains(response, 'Request was throttled', status_code=429)
from copy import deepcopy
import mock
try:
from functools import reduce
except ImportError: # python < 3
pass
try:
from unittest import mock
except ImportError: # python < 3
import mock
from django.conf import settings
from django.core.cache import caches
......
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')
This diff is collapsed.
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APIRequestFactory as rf
from events.models import Event, GrbEvent, Group, Pipeline, Search
from ..views import GrbEventPatchView
from ...settings import API_VERSION
UserModel = get_user_model()
###############################################################################
# UTILITIES ###################################################################
###############################################################################
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 create_grbevent(internal_group):
user = UserModel.objects.create(username='grbevent.creator')
grb_search, _ = Search.objects.get_or_create(name='GRB')
grbevent = GrbEvent.objects.create(
submitter=user,
group=Group.objects.create(name='External'),
pipeline=Pipeline.objects.create(name=settings.GRB_PIPELINES[0]),
search=grb_search
)
p, _ = Permission.objects.get_or_create(
content_type=ContentType.objects.get_for_model(GrbEvent),
codename='change_grbevent'
)
assign_perm(p, internal_group, grbevent)
return grbevent
###############################################################################
# FIXTURES ####################################################################
###############################################################################
###############################################################################
# TESTS #######################################################################
###############################################################################
@pytest.mark.django_db
def test_access(internal_user, internal_group, standard_plus_grb_user):
# NOTE: standard_plus_grb_user is a parametrized fixture (basically a
# list of three users), so this test will run three times.
# Create a GrbEvent
grbevent = create_grbevent(internal_group)
# Get URL and set up request and view
url = v_reverse("events:update-grbevent", args=[grbevent.graceid])
data = {'redshift': 2}
request = rf().patch(url, data=data)
request.user = standard_plus_grb_user
view = GrbEventPatchView.as_view()
with mock.patch('api.v1.events.views.EventAlertIssuer'):
# Process request
response = view(request, grbevent.graceid)
response.render()
# Update grbevent in memory from database
grbevent.refresh_from_db()
if standard_plus_grb_user.username != 'grb.user':
assert response.status_code == 403
assert grbevent.redshift is None
else:
assert response.status_code == 200
assert grbevent.redshift == 2
@pytest.mark.parametrize("data",
[
{'redshift': 2, 't90': 12, 'designation': 'good'},
{'ra': 1, 'dec': 2, 'error_radius': 3},
# FAR should not be updated
{'far': 123, 't90': 15},
]
)
@pytest.mark.django_db
def test_parameter_updates(grb_user, internal_group, data):
grbevent = create_grbevent(internal_group)
grbevent.far = 321
grbevent.save(update_fields=['far'])
# Get URL and set up request and view
url = v_reverse("events:update-grbevent", args=[grbevent.graceid])
request = rf().patch(url, data=data)
request.user = grb_user
view = GrbEventPatchView.as_view()
with mock.patch('api.v1.events.views.EventAlertIssuer'):
# Process request
response = view(request, grbevent.graceid)
response.render()
# Update grbevent in memory from database
grbevent.refresh_from_db()
# Check response
assert response.status_code == 200
# Compare parameters
for attr in GrbEventPatchView.updatable_attributes:
grbevent_attr = getattr(grbevent, attr)
if attr in data:
assert grbevent_attr == data.get(attr)
else:
assert grbevent_attr is None
# FAR should not be updated even by requests which include FAR
assert grbevent.far == 321
@pytest.mark.parametrize("data", [{}, {'redshift': 2}])
@pytest.mark.django_db
def test_update_with_no_new_data(grb_user, internal_group, data):
grbevent = create_grbevent(internal_group)
grbevent.redshift = 2
grbevent.save(update_fields=['redshift'])
# Get URL and set up request and view
url = v_reverse("events:update-grbevent", args=[grbevent.graceid])
request = rf().patch(url, data=data)
request.user = grb_user
view = GrbEventPatchView.as_view()
with mock.patch('api.v1.events.views.EventAlertIssuer'):
# Process request
response = view(request, grbevent.graceid)
response.render()
# Check response
assert response.status_code == 400
assert 'Request would not modify the GRB event' in response.content
@pytest.mark.parametrize("data",
[
{'redshift': 'random string'},
{'t90': 'random string'},
{'ra': 'random string'},
{'dec': 'random string'},
{'error_radius': 'random string'},
]
)
@pytest.mark.django_db
def test_update_with_bad_data(grb_user, internal_group, data):
grbevent = create_grbevent(internal_group)
# Get URL and set up request and view
url = v_reverse("events:update-grbevent", args=[grbevent.graceid])
request = rf().patch(url, data=data)
request.user = grb_user
view = GrbEventPatchView.as_view()
with mock.patch('api.v1.events.views.EventAlertIssuer'):
# Process request
response = view(request, grbevent.graceid)
response.render()
# Check response
assert response.status_code == 400
assert 'must be a float' in response.content
@pytest.mark.django_db
def test_update_non_grbevent(grb_user, internal_group):
event = Event.objects.create(
submitter=grb_user,
group=Group.objects.create(name='External'),
pipeline=Pipeline.objects.create(name='other_pipeline'),
)
p, _ = Permission.objects.get_or_create(
content_type=ContentType.objects.get_for_model(Event),
codename='change_event'
)
assign_perm(p, internal_group, event)
# Get URL and set up request and view
url = v_reverse("events:update-grbevent", args=[event.graceid])
request = rf().patch(url, data={'redshift': 2})
request.user = grb_user
view = GrbEventPatchView.as_view()
with mock.patch('api.v1.events.views.EventAlertIssuer'):
# Process request
response = view(request, event.graceid)
response.render()
# Check response
assert response.status_code == 400
assert 'Cannot update GRB event parameters for non-GRB event' \
in response.content
......@@ -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}]
......
This diff is collapsed.
......@@ -122,8 +122,8 @@ class GenericField(fields.Field):
return self.model.objects.get(**model_dict)
except self.model.DoesNotExist:
error_msg = '{model} with {lf}={data} does not exist' \
.format(model=self.model.__name__, lf=model_dict.keys()[0],
data=model_dict.values()[0])
.format(model=self.model.__name__, lf=list(model_dict)[0],
data=list(model_dict.values())[0])
raise exceptions.ValidationError(error_msg)
def get_model_dict(self, data):
......
......@@ -31,14 +31,18 @@ class TestPublicAccess(GraceDbApiTestBase):
"""Unauthenticated user can't access performance info"""
url = v_reverse('performance-info')
response = self.request_as_user(url, "GET")
self.assertEqual(response.status_code, 403)
self.assertIn("Authentication credentials were not provided",
response.content)
self.assertContains(
response,
'Authentication credentials were not provided',
status_code=403
)
def test_lvem_user_performance_info(self):
"""LV-EM user can't access performance info"""
url = v_reverse('performance-info')
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 403)
self.assertIn("Forbidden", response.content)
self.assertContains(
response,
'Forbidden',
status_code=403
)
......@@ -46,5 +46,5 @@ class TestUserInfoView(GraceDbApiTestBase):
self.assertEqual(response.status_code, 200)
# Test information
self.assertEqual(response.data.keys(), ['username'])
self.assertEqual(list(response.data), ['username'])
self.assertEqual(response.data['username'], 'AnonymousUser')
......@@ -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
......@@ -180,7 +185,7 @@ class PerformanceInfo(InheritDefaultPermissionsMixin, APIView):
try:
performance_info = get_performance_info()
except Exception, e:
except Exception as e:
return Response(str(e),
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
......
from collections import OrderedDict
import logging
import urllib
from django.utils.http import urlencode
from rest_framework import pagination
from rest_framework.response import Response
......@@ -80,7 +81,7 @@ class CustomSupereventPagination(pagination.LimitOffsetPagination):
'start': last,
self.limit_query_param: self.limit,
}
last_uri = base_uri + '?' + urllib.urlencode(param_dict)
last_uri = base_uri + '?' + urlencode(param_dict)
output = OrderedDict([
('numRows', numRows),
......
......@@ -58,7 +58,7 @@ class SupereventOrderingFilter(filters.OrderingFilter):
for f in fields:
prefix = '-' if f.startswith('-') else ''
f_s = f.lstrip('-')
if f_s in self.field_map.keys():
if f_s in self.field_map:
mapped_fields = self.field_map[f_s]
if not isinstance(mapped_fields, list):
mapped_fields = [mapped_fields]
......
from collections import OrderedDict
import logging
import urllib
from django.utils.http import urlencode
from rest_framework import pagination
from rest_framework.response import Response
......@@ -26,7 +27,7 @@ class CustomSupereventPagination(pagination.LimitOffsetPagination):
'start': last,
self.limit_query_param: self.limit,
}
last_uri = base_uri + '?' + urllib.urlencode(param_dict)
last_uri = base_uri + '?' + urlencode(param_dict)
output = OrderedDict([
('numRows', numRows),
......
......@@ -783,7 +783,7 @@ class SupereventEMObservationSerializer(serializers.ModelSerializer):
list_length = len(ra_list)
all_lists = (ra_list, dec_list, ra_width_list, dec_width_list,
start_time_list, duration_list)
if not all(map(lambda l: len(l) == list_length, all_lists)):
if not all(list(map(lambda l: len(l) == list_length, all_lists))):
self.fail('list_lengths')
return data
......
......@@ -45,12 +45,12 @@ class TestSupereventSerializerViaWeb(SupereventSetup, GraceDbApiTestBase):
response = self.request_as_user(url, "GET", self.internal_user)
self.assertEqual(response.status_code, 200)
# Check data on page
response_keys = response.json().keys()
response_keys = list(response.json())
response_links = response.json()['links']
self.assertIn('preferred_event', response_keys)
self.assertIn('gw_events', response_keys)
self.assertIn('em_events', response_keys)
self.assertIn('events', response_links.keys())
self.assertIn('events', response_links)
def test_lvem_user_get_superevent_detail(self):
"""LV-EM user sees events link and all event graceids"""
......@@ -61,12 +61,12 @@ class TestSupereventSerializerViaWeb(SupereventSetup, GraceDbApiTestBase):
response = self.request_as_user(url, "GET", self.lvem_user)
self.assertEqual(response.status_code, 200)
# Check data on page
response_keys = response.json().keys()
response_keys = list(response.json())
response_links = response.json()['links']
self.assertIn('preferred_event', response_keys)
self.assertIn('gw_events', response_keys)
self.assertIn('em_events', response_keys)
self.assertIn('events', response_links.keys())
self.assertIn('events', response_links)
def test_public_user_get_superevent_detail(self):
"""Public user does not see events link or all event graceids"""
......@@ -77,9 +77,9 @@ class TestSupereventSerializerViaWeb(SupereventSetup, GraceDbApiTestBase):
response = self.request_as_user(url, "GET")
self.assertEqual(response.status_code, 200)
# Check data on page
response_keys = response.json().keys()
response_keys = list(response.json())
response_links = response.json()['links']
self.assertNotIn('preferred_event', response_keys)
self.assertNotIn('gw_events', response_keys)
self.assertNotIn('em_events', response_keys)
self.assertNotIn('events', response_links.keys())
self.assertNotIn('events', response_links)
......@@ -60,12 +60,12 @@ def construct_url_templates(request=None):
# keys are like '{view_name}-template'
# values are URLs with placeholder parameters
templates = {view_name + '-template': sr(view_name, args=args)
for view_name, args in views.iteritems()}
for view_name, args in views.items()}
# Replace URL placeholder parameters with string formatting placeholders
# Ex: replace 'G1234' with '{graceid}'
for k,v in templates.iteritems():
for pattern,placeholder in PH.iteritems():
for k,v in templates.items():
for pattern,placeholder in PH.items():
if placeholder in v:
v = v.replace(placeholder, "{{{0}}}".format(pattern))
templates[k] = v
......
......@@ -15,7 +15,7 @@ from rest_framework.views import APIView
from core.file_utils import get_file_list
from core.http import check_and_serve_file
from core.vfile import VersionedFile, FileVersionError, FileVersionNameError
from core.vfile import FileVersionError, FileVersionNameError
from events.models import Event, Label
from events.view_utils import reverse as gracedb_reverse
from ligoauth.utils import is_internal
......
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
from django.contrib.auth.models import (
Group, Permission, AnonymousUser,
)
from django.contrib.contenttypes.models import ContentType
# Groups ----------------------------------------------------------------------
@pytest.mark.django_db
......@@ -37,6 +43,7 @@ def internal_group():
return group
@pytest.mark.django_db
@pytest.fixture
def em_advocates_group():
......@@ -61,6 +68,27 @@ def em_advocates_group():
return group
@pytest.mark.django_db
@pytest.fixture
def grb_managers_group():
group, _ = Group.objects.get_or_create(name='grb_managers')
# Add permissions
perm_data = [
{'model': 'grbevent', 'codename': 't90_grbevent'},
]
permission_list = []
for perm in perm_data:
p, _ = Permission.objects