Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • alexander.pace/server
  • geoffrey.mo/gracedb-server
  • deep.chatterjee/gracedb-server
  • cody.messick/server
  • sushant.sharma-chaudhary/server
  • michael-coughlin/server
  • daniel.wysocki/gracedb-server
  • roberto.depietri/gracedb
  • philippe.grassia/gracedb
  • tri.nguyen/gracedb
  • jonah-kanner/gracedb
  • brandon.piotrzkowski/gracedb
  • joseph-areeda/gracedb
  • duncanmmacleod/gracedb
  • thomas.downes/gracedb
  • tanner.prestegard/gracedb
  • leo-singer/gracedb
  • computing/gracedb/server
18 results
Show changes
Showing
with 1943 additions and 427 deletions
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')
from django.conf import settings
from rest_framework import serializers
from rest_framework.fields import CurrentUserDefault
from events.models import Event, EventLog, GrbEvent, NeutrinoEvent, \
CoincInspiralEvent, MLyBurstEvent, MultiBurstEvent, LalInferenceBurstEvent, \
SingleInspiral, SimInspiralEvent
from api.utils import api_reverse
from superevents.models import Superevent
# Fields in the siminspiral table to expose to public:
snglinsp_public_fields = ['ifo','end_time','end_time_ns']
multiburst_public_fields = ['ifos', 'single_ifo_times']
# define a function that returns a tuple of the superevent window range:
def superevent_window_range(gpstime):
return (gpstime - settings.EVENT_SUPEREVENT_WINDOW_BEFORE, \
gpstime + settings.EVENT_SUPEREVENT_WINDOW_AFTER)
class GRBEventSerializer(serializers.ModelSerializer):
T90 = serializers.SerializerMethodField()
class Meta:
model = GrbEvent
fields =('author_ivorn', 'dec', 'designation', 'redshift', \
'how_description', 'coord_system', 'trigger_id', 'error_radius', \
'how_reference_url', 'ra', 'ivorn', 'trigger_duration', \
'author_shortname', 'T90', 'observatory_location_id')
def get_T90(self, obj):
return obj.t90
class NeutrinoEventSerializer(serializers.ModelSerializer):
class Meta:
model = NeutrinoEvent
fields =('ivorn', 'coord_system', 'ra', 'dec', 'error_radius', \
'far_ne', 'far_unit', 'signalness', 'energy', 'src_error_90', \
'src_error_50', 'amon_id', 'run_id', 'event_id', 'stream')
class CoincInspiralEventSerializer(serializers.ModelSerializer):
class Meta:
model = CoincInspiralEvent
fields = ('ifos', 'end_time', 'end_time_ns', 'mass', 'mchirp',
'minimum_duration', 'snr', 'false_alarm_rate', 'combined_far')
class MLyBurstEventSerializer(serializers.ModelSerializer):
scores = serializers.SerializerMethodField()
SNR = serializers.SerializerMethodField()
class Meta:
model = MLyBurstEvent
fields =('ifos', 'central_freq', 'bandwidth', 'duration', 'central_time', \
'detection_statistic', 'SNR', 'bbh', 'sglf', 'sghf', \
'background', 'glitch', 'freq_correlation', 'channels', 'scores')
def to_representation(self, obj):
channels_out = None
ret = super().to_representation(obj)
if obj.channels:
channels_out = obj.channels.split(',')
ret['channels'] = channels_out
return ret
def get_scores(self, obj):
return {'coherency': obj.score_coher,
'coincidence': obj.score_coinc,
'combined': obj.score_comb}
def get_SNR(self, obj):
return obj.snr
class MultiBurstEventSerializer(serializers.ModelSerializer):
class Meta:
model = MultiBurstEvent
fields =('ifos', 'start_time', 'start_time_ns', 'duration', 'strain', \
'peak_time', 'peak_time_ns', 'central_freq', 'bandwidth', \
'amplitude', 'mchirp', 'snr', 'confidence', 'false_alarm_rate', \
'ligo_axis_ra', 'ligo_axis_dec', 'ligo_angle', 'ligo_angle_sig', \
'single_ifo_times', 'hoft', 'code')
def __init__(self, *args, is_external=False, **kwargs):
super().__init__(*args, **kwargs)
if is_external:
self.fields = {field_name: self.fields[field_name] \
for field_name in multiburst_public_fields}
class LalInferenceBurstEventSerializer(serializers.ModelSerializer):
class Meta:
model = LalInferenceBurstEvent
fields =('bci', 'quality_mean', 'quality_median', 'bsn', \
'omicron_snr_network', 'omicron_snr_H1', 'omicron_snr_L1', \
'omicron_snr_V1', 'hrss_mean', 'hrss_median', 'frequency_mean', \
'frequency_median')
class SimInspiralEventSerializer(serializers.ModelSerializer):
class Meta:
model = SimInspiralEvent
fields =('mass1', 'mass2', 'eta', 'coa_phase', 'mchirp', 'spin1x', \
'spin1y', 'spin1z', 'spin2x', 'spin2y', 'spin2z', 'end_time_gmst', \
'f_lower', 'f_final', 'distance', 'latitude', 'longitude', \
'polarization', 'inclination', 'theta0', 'phi0', 'alpha', 'beta', \
'psi0', 'psi3', 'alpha1', 'alpha2', 'alpha3', 'alpha4', 'alpha5', \
'alpha6', 'eff_dist_g', 'eff_dist_h', 'eff_dist_l', 'eff_dist_t', \
'eff_dist_v', 'amplitude', 'tau', 'phi', 'freq', 'amp_order', \
'geocent_end_time', 'geocent_end_time_ns', 'numrel_mode_min', \
'numrel_mode_max', 'bandpass', 'g_end_time', 'g_end_time_ns', \
'h_end_time', 'h_end_time_ns', 'l_end_time', 'l_end_time_ns', \
't_end_time', 't_end_time_ns', 'v_end_time', 'v_end_time_ns', \
'waveform', 'numrel_data', 'source', 'taper', 'source_channel', \
'destination_channel')
class SingleInspiralSerializer(serializers.ModelSerializer):
class Meta:
model = SingleInspiral
fields = ('ifo', 'search', 'channel', 'end_time', 'end_time_ns', \
'end_time_gmst', 'impulse_time', 'impulse_time_ns', \
'template_duration', 'event_duration', 'amplitude', \
'eff_distance', 'coa_phase', 'mass1', 'mass2', 'mchirp', \
'mtotal', 'eta', 'kappa', 'chi', 'tau0', 'tau2', 'tau3', \
'tau4', 'tau5', 'ttotal', 'psi0', 'psi3', 'alpha', \
'alpha1', 'alpha2', 'alpha3', 'alpha4', 'alpha5', 'alpha6', \
'beta', 'f_final', 'snr', 'chisq', 'chisq_dof', 'bank_chisq', \
'bank_chisq_dof', 'cont_chisq', 'cont_chisq_dof', 'sigmasq', \
'rsqveto_duration', 'Gamma0', 'Gamma1', 'Gamma2', 'Gamma3', \
'Gamma4', 'Gamma5', 'Gamma6', 'Gamma7', 'Gamma8', 'Gamma9', \
'spin1x', 'spin1y', 'spin1z', 'spin2x', 'spin2y', 'spin2z')
def __init__(self, *args, is_external=False, **kwargs):
super().__init__(*args, **kwargs)
if is_external:
self.fields = {field_name: self.fields[field_name] \
for field_name in snglinsp_public_fields}
# This dict maps the key in the extra_attributes dict to its corresponding
# event subclass name. SingleInspiral events are a little different, so do
# that separately for now.
EVENT_ATTRIBUTE_MAP = {
GrbEvent: ('GRB', 'grbevent', GRBEventSerializer),
NeutrinoEvent: ('NeutrinoEvent', 'neutrinoevent', NeutrinoEventSerializer),
CoincInspiralEvent: ('CoincInspiral', 'coincinspiralevent', CoincInspiralEventSerializer),
MLyBurstEvent: ('MLyBurst', 'mlyburstevent', MLyBurstEventSerializer),
MultiBurstEvent: ('MultiBurst', 'multiburstevent', MultiBurstEventSerializer),
LalInferenceBurstEvent: ('LalInferenceBurst', 'lalinferenceburstevent', LalInferenceBurstEventSerializer),
SimInspiralEvent: ('SimInspiral', 'siminspiralevent', SimInspiralEventSerializer),
}
class EventSerializer(serializers.ModelSerializer):
# Fields.
group = serializers.CharField(source="group.name")
pipeline = serializers.CharField(source="pipeline.name")
search = serializers.CharField(source="search.name", allow_null=True)
submitter = serializers.SlugRelatedField(slug_field="username",
read_only=True)
# New fields.
labels = serializers.SerializerMethodField('get_labels')
created = serializers.DateTimeField(format=settings.GRACE_STRFTIME_FORMAT,
read_only=True)
far_is_upper_limit = serializers.SerializerMethodField()
extra_attributes = serializers.SerializerMethodField(allow_null=True)
links = serializers.SerializerMethodField('get_links')
superevent_neighbours = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super(EventSerializer, self).__init__(*args, **kwargs)
self.request = self.context.get('request', None)
self.external = self.context.get('request_is_external', False)
# don't include neighboring superevents if this is nested inside itself
self.is_nested = self.context.get('is_nested', False)
# don't include neighboring superevents for serializations that aren't
# alerts.
self.is_alert = self.context.get('is_alert', False)
if not self.is_alert or self.is_nested:
self.fields.pop('superevent_neighbours')
class Meta:
model = Event
fields = ('submitter', 'created', 'group',
'pipeline', 'graceid', 'gpstime', 'reporting_latency',
'instruments', 'nevents', 'offline',
'search', 'far', 'far_is_upper_limit', 'likelihood',
'labels', 'extra_attributes', 'superevent', 'links',
'superevent_neighbours')
def to_representation(self, obj):
display_far, far_is_upper_limit = self.display_far_and_limit(obj)
ret = super().to_representation(obj)
ret['far'] = display_far
ret['superevent'] = obj.superevent.superevent_id if obj.superevent else None
ret['far_is_upper_limit'] = far_is_upper_limit
return ret
# modify the display far and limit parameter. TODO this same code is
# duplicated in at least (?) two other places, it should really get
# combined into one function. really though, it's probably not even
# necessary since the VOEVENT_FAR_FLOOR is zero (and so not used...)
# but who knows, it might come back at some point.
def display_far_and_limit(self, obj):
far_is_upper_limit = False
display_far = obj.far
if obj.far and self.external and obj.far < settings.VOEVENT_FAR_FLOOR:
display_far = settings.VOEVENT_FAR_FLOOR
far_is_upper_limit = True
return display_far, far_is_upper_limit
def get_labels(self, obj):
return [label.name for label in obj.labels.all()]
def display_far_floor(self, obj):
if obj.far and self.external and obj.far < settings.VOEVENT_FAR_FLOOR:
return True
else:
return False
def get_links(self, obj):
graceid = obj.graceid
return {
"neighbors" : api_reverse("events:neighbors", args=[graceid], request=self.request),
"log" : api_reverse("events:eventlog-list", args=[graceid], request=self.request),
"emobservations" : api_reverse("events:emobservation-list", args=[graceid], request=self.request),
"files" : api_reverse("events:files", args=[graceid], request=self.request),
"labels" : api_reverse("events:labels", args=[graceid], request=self.request),
"self" : api_reverse("events:event-detail", args=[graceid], request=self.request),
"tags" : api_reverse("events:eventtag-list", args=[graceid], request=self.request),
}
# This is a dummy placeholder since it gets overwritten by to_representation
def get_far_is_upper_limit(self, obj):
return False
def get_extra_attributes(self, obj):
extra_attrs_dict = {}
# Include this section for requests by internal users and
# FIXME alert contents:
if (self.request is not None and not self.external) or self.is_alert or self.is_nested:
for subevent_type, subevent_vals in EVENT_ATTRIBUTE_MAP.items():
if isinstance(obj, subevent_type):
extra_attrs_dict.update({subevent_vals[0]:
subevent_vals[2](getattr(obj, subevent_vals[1])).data})
# For CoincInspiral events, append the SingleInspiral data, after
# checking to see if the tables are actually there, just to be sure.
if isinstance(obj, CoincInspiralEvent):
if obj.singleinspiral_set.exists():
extra_attrs_dict.update({'SingleInspiral':
[SingleInspiralSerializer(e, is_external=self.external).data for e in obj.singleinspiral_set.all()]
})
# For the special inexplicable case where the user is external, then
# return specific fields from SingleInspiral or multiburst events
elif (self.request is not None and self.external):
if isinstance(obj, CoincInspiralEvent):
extra_attrs_dict.update({'SingleInspiral':
[SingleInspiralSerializer(e, is_external=self.external).data \
for e in obj.singleinspiral_set.all()]})
elif isinstance(obj, MultiBurstEvent):
extra_attrs_dict.update({'MultiBurst':
MultiBurstEventSerializer(obj, is_external=self.external).data})
return extra_attrs_dict
def get_superevent_neighbours(self, obj):
return_dict = {}
# Look for neighbors only if the event has a valid gpstime. This will
# probably trigger if event.gpstime=0.0, but that event isn't valid anyway.
if obj.gpstime:
for s in self.superevent_neighbours_set(obj):
sn_dict = {}
# Add superevent_id:
sn_dict.update({
'superevent_id': s.superevent_id,
})
# Add gw_events:
sn_dict.update({
'gw_events': [g.graceid for g in s.get_internal_events()],
})
# Add basic superevent info:
sn_dict.update({
'far': s.far,
't_start': s.t_start,
't_0': s.t_0,
't_end': s.t_end,
})
# Add labels:
sn_dict.update({
'labels': [l.name for l in s.labels.all()],
})
# Add preferred_event:
sn_dict.update({
'preferred_event': s.preferred_event.graceid,
})
# Add preferred_event_data:
sn_dict.update({
'preferred_event_data':
EventSerializer(s.preferred_event.get_subclass(),
context={'is_nested': True}).data,
})
# Add this info to the response
return_dict.update(
{s.superevent_id: sn_dict})
return return_dict
def superevent_category(self, obj):
# return the corresponding superevent category of the event
if obj.is_production():
return Superevent.SUPEREVENT_CATEGORY_PRODUCTION
elif obj.is_mdc():
return Superevent.SUPEREVENT_CATEGORY_MDC
elif obj.is_test():
return Superevent.SUPEREVENT_CATEGORY_TEST
def superevent_neighbours_set(self, obj):
# query the database for nearby superevents
return Superevent.objects.filter(
t_0__range=superevent_window_range(obj.gpstime),
category=self.superevent_category(obj)) \
.prefetch_related('events', 'labels') \
.select_related('preferred_event', 'preferred_event__pipeline',
'preferred_event__group',
'preferred_event__search',
'preferred_event__submitter',
'preferred_event__superevent',
'preferred_event__grbevent',
'preferred_event__neutrinoevent',
'preferred_event__coincinspiralevent',
'preferred_event__mlyburstevent',
'preferred_event__multiburstevent',
'preferred_event__lalinferenceburstevent',
'preferred_event__siminspiralevent',
)
class EventLogSerializer(serializers.ModelSerializer):
"""docstring for EventLogSerializer"""
comment = serializers.CharField(required=True, max_length=200)
class Meta:
model = EventLog
fields = ('comment', 'issuer', 'created')
......@@ -22,9 +22,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "POST"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_detail(self):
"""Unauthenticated user can't access event detail"""
......@@ -32,9 +34,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "PUT"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_log_list(self):
"""Unauthenticated user can't access event log list"""
......@@ -42,9 +46,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "POST"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_log_detail(self):
"""Unauthenticated user can't access event log detail"""
......@@ -52,9 +58,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_voevent_list(self):
"""Unauthenticated user can't access event VOEvent list"""
......@@ -62,9 +70,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "POST"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_voevent_detail(self):
"""Unauthenticated user can't access event VOEvent detail"""
......@@ -72,9 +82,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_embbeventlog_list(self):
"""Unauthenticated user can't access event EMBBEventLog list"""
......@@ -82,9 +94,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "POST"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_embbeventlog_detail(self):
"""Unauthenticated user can't access event EMBBEventLog detail"""
......@@ -92,9 +106,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_emobservation_list(self):
"""Unauthenticated user can't access event EMObservation list"""
......@@ -102,9 +118,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "POST"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_emobservation_detail(self):
"""Unauthenticated user can't access event EMObservation detail"""
......@@ -112,9 +130,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_tag_list(self):
"""Unauthenticated user can't access event tag list"""
......@@ -122,9 +142,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_tag_detail(self):
"""Unauthenticated user can't access event tag detail"""
......@@ -132,9 +154,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_log_tag_list(self):
"""Unauthenticated user can't access event log tag list"""
......@@ -142,9 +166,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_log_tag_detail(self):
"""Unauthenticated user can't access event log tag detail"""
......@@ -153,9 +179,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "PUT", "DELETE"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_permission_list(self):
"""Unauthenticated user can't access event permission list"""
......@@ -163,9 +191,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_group_permission_list(self):
"""Unauthenticated user can't access event group permission list"""
......@@ -174,9 +204,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_group_permission_detail(self):
"""Unauthenticated user can't access event group permission list"""
......@@ -185,9 +217,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "PUT", "DELETE"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_files(self):
"""Unauthenticated user can't access event files (list or detail)"""
......@@ -195,9 +229,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "PUT"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_labels(self):
"""Unauthenticated user can't access event labels (list or detail)"""
......@@ -205,9 +241,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET", "PUT", "DELETE"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_neighbors(self):
"""Unauthenticated user can't access event neighbors list"""
......@@ -215,9 +253,11 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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_event_signoff_list(self):
"""Unauthenticated user can't access event signoff list"""
......@@ -225,7 +265,8 @@ class TestPublicAccess(EventSetup, GraceDbApiTestBase):
methods = ["GET"]
for http_method in methods:
response = self.request_as_user(url, http_method)
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
)
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from rest_framework.exceptions import ValidationError
from ..fields import EventGraceidField
@pytest.mark.parametrize(
"graceid",
[1234, 1.234, (), [], None, True, lambda x: x]
)
def test_bad_types(graceid):
field = EventGraceidField()
err_msg = 'Event graceid must be a string.'
with pytest.raises(ValidationError, match=err_msg):
field.to_internal_value(graceid)
@pytest.mark.parametrize(
"graceid",
['GG', '1234G', 'G.1234', 'G1234z', 'Q1234', 'GH12']
)
def test_graceid_bad_format(graceid):
field = EventGraceidField()
err_msg = 'Not a valid graceid.'
with pytest.raises(ValidationError, match=err_msg):
field.to_internal_value(graceid)
@pytest.mark.parametrize(
"graceid",
['G1234', 'E0001', 'H12', 'M352345', 'T2323', ' T123', 'T123 ', ' T123 ',
'g4567', 't456 ', ' m2398 ', ' e8732']
)
def test_valid_graceids(graceid):
field = EventGraceidField()
# WHY do we have to mock this as 'gracedb.api...'
# instead of just 'api...'??
super_tiv = 'gracedb.api.v1.fields.GenericField.to_internal_value'
with mock.patch(super_tiv) as mock_super_tiv:
field.to_internal_value(graceid)
call_args, _ = mock_super_tiv.call_args
assert mock_super_tiv.call_count == 1
assert len(call_args) == 1
assert call_args[0] == graceid.upper().strip()
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
)
grbevent.save()
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('gracedb.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('gracedb.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('gracedb.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.decode()
@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('gracedb.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.decode()
@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'),
)
event.save()
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('gracedb.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.decode()
from django.conf.urls import url, include
from django.urls import re_path, include
# Turn off api caching:
from django.views.decorators.cache import never_cache
from .views import *
......@@ -6,74 +8,76 @@ from .views import *
urlpatterns = [
# Event Resources
# events/[{graceid}[/{version}]]
url(r'^$', EventList.as_view(), name='event-list'),
url(r'^(?P<graceid>[GEHMT]\d+)$', EventDetail.as_view(),
re_path(r'^$', never_cache(EventList.as_view()), name='event-list'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)$', never_cache(EventDetail.as_view()),
name='event-detail'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/update-grbevent/$',
never_cache(GrbEventPatchView.as_view()), name='update-grbevent'),
# Event Log Resources
# events/{graceid}/logs/[{logid}]
url(r'^(?P<graceid>[GEHMT]\d+)/log/$', EventLogList.as_view(),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/log/$', never_cache(EventLogList.as_view()),
name='eventlog-list'),
url(r'^(?P<graceid>[GEHMT]\d+)/log/(?P<n>\d+)$',
EventLogDetail.as_view(), name='eventlog-detail'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/log/(?P<n>\d+)$',
never_cache(EventLogDetail.as_view()), name='eventlog-detail'),
# VOEvent Resources
# events/{graceid}/voevent/[{serial_number}]
url(r'^(?P<graceid>[GEHMT]\d+)/voevent/$', VOEventList.as_view(),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/voevent/$', never_cache(VOEventList.as_view()),
name='voevent-list'),
url(r'^(?P<graceid>[GEHMT]\d+)/voevent/(?P<n>\d+)$',
VOEventDetail.as_view(), name='voevent-detail'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/voevent/(?P<n>\d+)$',
never_cache(VOEventDetail.as_view()), name='voevent-detail'),
# EMBB Resources
# events/{graceid}/logs/[{logid}]
url(r'^(?P<graceid>[GEHMT]\d+)/embb/$', EMBBEventLogList.as_view(),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/embb/$', never_cache(EMBBEventLogList.as_view()),
name='embbeventlog-list'),
url(r'^(?P<graceid>[GEHMT]\d+)/embb/(?P<n>\d+)$',
EMBBEventLogDetail.as_view(), name='embbeventlog-detail'),
url(r'^(?P<graceid>[GEHMT]\d+)/emobservation/$',
EMObservationList.as_view(), name='emobservation-list'),
url(r'^(?P<graceid>[GEHMT]\d+)/emobservation/(?P<n>\d+)$',
EMObservationDetail.as_view(), name='emobservation-detail'),
# url(r'(?P<graceid>[GEHMT]\d+)/emobservation/(?P<n>\d+)/emfootprint/$',
re_path(r'^(?P<graceid>[GEHMTD]\d+)/embb/(?P<n>\d+)$',
never_cache(EMBBEventLogDetail.as_view()), name='embbeventlog-detail'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/emobservation/$',
never_cache(EMObservationList.as_view()), name='emobservation-list'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/emobservation/(?P<n>\d+)$',
never_cache(EMObservationDetail.as_view()), name='emobservation-detail'),
# re_path(r'(?P<graceid>[GEHMTD]\d+)/emobservation/(?P<n>\d+)/emfootprint/$',
# EMFootprintList.as_view(), name='emfootprint-list'),
# url(r'(?P<graceid>[GEHMT]\d+)/emobservation/(?P<n>\d+)/emfootprint/(?P<m>\d+)$',
# re_path(r'(?P<graceid>[GEHMTD]\d+)/emobservation/(?P<n>\d+)/emfootprint/(?P<m>\d+)$',
# EMFootprintDetail.as_view(), name='emfootprint-detail'),
# Tag Resources
url(r'^(?P<graceid>[GEHMT]\d+)/tag/$', EventTagList.as_view(),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/tag/$', never_cache(EventTagList.as_view()),
name='eventtag-list'),
url(r'^(?P<graceid>[GEHMT]\d+)/tag/(?P<tagname>.+)$',
EventTagDetail.as_view(), name='eventtag-detail'),
url(r'^(?P<graceid>[GEHMT]\d+)/log/(?P<n>\d+)/tag/$',
EventLogTagList.as_view(), name='eventlogtag-list'),
url(r'^(?P<graceid>[GEHMT]\d+)/log/(?P<n>\d+)/tag/(?P<tagname>.+)$',
EventLogTagDetail.as_view(), name='eventlogtag-detail'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/tag/(?P<tagname>.+)$',
never_cache(EventTagDetail.as_view()), name='eventtag-detail'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/log/(?P<n>\d+)/tag/$',
never_cache(EventLogTagList.as_view()), name='eventlogtag-list'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/log/(?P<n>\d+)/tag/(?P<tagname>.+)$',
never_cache(EventLogTagDetail.as_view()), name='eventlogtag-detail'),
# Permission Resources
url(r'^(?P<graceid>[GEHMT]\d+)/perms/$',
EventPermissionList.as_view(), name='eventpermission-list'),
url(r'^(?P<graceid>[GEHMT]\d+)/perms/(?P<group_name>.+)/$',
GroupEventPermissionList.as_view(), name='groupeventpermission-list'),
url(r'^(?P<graceid>[GEHMT]\d+)/perms/(?P<group_name>.+)/(?P<perm_shortname>\w+)$',
GroupEventPermissionDetail.as_view(), name='groupeventpermission-detail'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/perms/$',
never_cache(EventPermissionList.as_view()), name='eventpermission-list'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/perms/(?P<group_name>.+)/$',
never_cache(GroupEventPermissionList.as_view()), name='groupeventpermission-list'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/perms/(?P<group_name>.+)/(?P<perm_shortname>\w+)$',
never_cache(GroupEventPermissionDetail.as_view()), name='groupeventpermission-detail'),
# Event File Resources
# events/{graceid}/files/[{filename}[/{version}]]
url(r'^(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$',
Files.as_view(), name="files"),
re_path(r'^(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$',
never_cache(Files.as_view()), name="files"),
# Event Labels
# events/{graceid}/labels/[{label}]
url(r'^(?P<graceid>\w[\d]+)/labels/(?P<label>.+)?$',
EventLabel.as_view(), name="labels"),
re_path(r'^(?P<graceid>\w[\d]+)/labels/(?P<label>.+)?$',
never_cache(EventLabel.as_view()), name="labels"),
# Event Neighbors
# events/{graceid}/neighbors/[?delta=(N|(N,N))]
url(r'^(?P<graceid>\w[\d]+)/neighbors/$', EventNeighbors.as_view(),
re_path(r'^(?P<graceid>\w[\d]+)/neighbors/$', never_cache(EventNeighbors.as_view()),
name="neighbors"),
# Operator Signoff Resources
url(r'^(?P<graceid>[GEHMT]\d+)/signoff/$',
OperatorSignoffList.as_view(), name='signoff-list'),
re_path(r'^(?P<graceid>[GEHMTD]\d+)/signoff/$',
never_cache(OperatorSignoffList.as_view()), name='signoff-list'),
]
from __future__ import absolute_import
import exceptions
import json
import logging
import os
import shutil
import StringIO
import urllib
try:
from StringIO import StringIO
except ImportError: # python >= 3
from io import StringIO
from django.conf import settings
from django.contrib.auth.models import User, Permission, Group as AuthGroup
from django.contrib.auth.models import User, Permission, Group as DjangoGroup
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.exceptions import ValidationError, FieldError
from django.db import IntegrityError
from django.http import HttpResponse, HttpResponseForbidden, \
HttpResponseNotFound, HttpResponseServerError
HttpResponseNotFound, HttpResponseServerError, HttpResponseBadRequest
from django.http.request import QueryDict
from django.utils.functional import wraps
# Stuff for the LigoLwRenderer
from glue.ligolw import ligolw
# Stuff for the LigoLwRenderer (converted from glue to ligo.lw)
from ligo.lw import ligolw
# lsctables MUST be loaded before utils.
from glue.ligolw import utils
from glue.ligolw.utils import ligolw_add
from glue.ligolw.ligolw import LIGOLWContentHandler
from glue.ligolw.lsctables import use_in
from ligo.lw import utils
from ligo.lw.utils import ligolw_add
from core.ligolw import ThoroughFlexibleContentHandler
from ligo.lw.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 import authentication, parsers, \
serializers, status
from rest_framework.exceptions import ValidationError as DrfValidationError, \
ErrorDetail
from rest_framework.permissions import IsAuthenticated, BasePermission, \
SAFE_METHODS
from rest_framework.renderers import BaseRenderer, JSONRenderer, \
BrowsableAPIRenderer
from rest_framework.response import Response
......@@ -34,13 +40,13 @@ from rest_framework.views import APIView
from alerts.issuers.events import EventAlertIssuer, EventLogAlertIssuer, \
EventVOEventAlertIssuer, EventPermissionsAlertIssuer
from annotations.voevent_utils import construct_voevent_file
from api.throttling import BurstAnonRateThrottle
from core.http import check_and_serve_file
from core.vfile import VersionedFile
from events.buildVOEvent import buildVOEvent, VOEventBuilderException
from core.vfile import create_versioned_file
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 +59,9 @@ 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 .paginators import CustomEventPagination
from .serializers import EventSerializer, EventLogSerializer
from .throttling import EventCreationThrottle, AnnotationThrottle
from ..mixins import InheritDefaultPermissionsMixin
from ...utils import api_reverse
......@@ -61,12 +70,23 @@ from ...utils import api_reverse
logger = logging.getLogger(__name__)
# Set up content handler
use_in(LIGOLWContentHandler)
use_in(ThoroughFlexibleContentHandler)
# For checking queries in the event that the user is external
REST_FRAMEWORK_SETTINGS = getattr(settings, 'REST_FRAMEWORK', {})
PAGINATE_BY = REST_FRAMEWORK_SETTINGS.get('PAGINATE_BY', 10)
# a "temporary" error message:
xml_err_msg = ('ligolw-xml rendering has been disabled, please use the '
'ligo-gracedb API to download event coinc xml data.')
# parameters for select_related:
event_related_objects = ('group', 'pipeline', 'search', 'submitter',
'superevent')
event_prefetch_objects = ('labels', 'grbevent', \
'neutrinoevent', 'coincinspiralevent', 'mlyburstevent', \
'multiburstevent', 'lalinferenceburstevent', 'siminspiralevent',
'singleinspiral_set')
# Custom APIView class for inheriting default permissions
class InheritPermissionsAPIView(InheritDefaultPermissionsMixin, APIView):
......@@ -82,7 +102,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
......@@ -146,7 +166,7 @@ def group_required(view):
@wraps(view)
def inner(self, request, event, group_name, *args, **kwargs):
try:
group = AuthGroup.objects.get(name=str(group_name))
group = DjangoGroup.objects.get(name=str(group_name))
except:
return Response("Group does not exist.",
status=status.HTTP_404_NOT_FOUND)
......@@ -174,52 +194,6 @@ def event_perm_object_required(view):
return view(self, request, event, group, permission, *args, **kwargs)
return inner
#class EventSerializer(serializers.ModelSerializer):
# # Overloaded fields.
# group = serializers.CharField(source="group.name")
# submitter = serializers.CharField(source="submitter.name")
# graceid = serializers.Field(source="graceid")
# analysisType = serializers.Field(source="get_analysisType_display")
#
# # New fields.
# labels = serializers.SerializerMethodField('get_labels')
# links = serializers.SerializerMethodField('get_links')
#
# class Meta:
# model = Event
# fields = ('submitter', 'created', 'group', 'graceid',
# 'analysisType', 'gpstime', 'instruments',
# 'nevents', 'far', 'likelihood', 'labels',
# 'links',)
#
# def get_labels(self,obj):
# request = self.context['request']
# graceid = obj.graceid
# return dict([
# (labelling.label.name,
# reverse("labels",
# args=[graceid, labelling.label.name],
# request=request))
# for labelling in obj.labelling_set.all()])
#
# def get_links(self,obj):
# request = self.context['request']
# graceid = obj.graceid
# return {
# "neighbors" : reverse("neighbors", args=[graceid], request=request),
# "log" : reverse("eventlog-list", args=[graceid], request=request),
# "files" : reverse("files", args=[graceid], request=request),
# "labels" : reverse("labels", args=[graceid], request=request),
# "self" : reverse("event-detail", args=[graceid], request=request),
# "tags" : reverse("eventtag-list", args=[graceid], request=request),
# }
class EventLogSerializer(serializers.ModelSerializer):
"""docstring for EventLogSerializer"""
comment = serializers.CharField(required=True, max_length=200)
class Meta:
model = EventLog
fields = ('comment', 'issuer', 'created')
#==================================================================
# Custom renderers and various accoutrements
......@@ -240,7 +214,8 @@ class CoincAccess(Exception):
return repr(self.detail)
def assembleLigoLw(data):
if 'events' in data.keys():
# data is a dict
if 'events' in data:
eventDictList = data['events']
else:
# There is only one event.
......@@ -252,7 +227,7 @@ def assembleLigoLw(data):
raise MissingCoinc
elif not os.access(fname, os.R_OK):
raise CoincAccess
utils.load_filename(fname, xmldoc=xmldoc, contenthandler=LIGOLWContentHandler)
utils.load_filename(fname, xmldoc=xmldoc, contenthandler=ThoroughFlexibleContentHandler)
ligolw_add.reassign_ids(xmldoc)
ligolw_add.merge_ligolws(xmldoc)
ligolw_add.merge_compatible_tables(xmldoc)
......@@ -261,17 +236,27 @@ def assembleLigoLw(data):
class LigoLwRenderer(BaseRenderer):
media_type = 'application/xml'
format = 'xml'
error_xml = '<?xml version="1.0" encoding="UTF-8" ?>'\
'<root><detail>{message}</detail>'\
'</root>'
def render(self, data, media_type=None, renderer_context=None):
# XXX If there was an error, we will return the error message
# in plain text, effectively ignoring the accepts header.
# Somewhat irregular?
if 'error' in data.keys():
return data['error']
# Somewhat irregular? edit: i would argue probably impossible, but
# let's fix this anyway.
if 'error' in data:
return self.error_xml.format(message=data['error'])
# If data contains an expected error message, return the error, like
# if there is no authentication. Error is still in xml format since
# that's what browsers are expecting to render.
if isinstance(data.get('detail', None), ErrorDetail):
return self.error_xml.format(message=data['detail'])
xmldoc = assembleLigoLw(data)
# XXX Aaargh! Just give me the contents of the xml doc. Annoying.
output = StringIO.StringIO()
output = StringIO()
xmldoc.write(output)
return output.getvalue()
......@@ -280,12 +265,12 @@ class TSVRenderer(BaseRenderer):
format = 'tsv'
def render(self, data, media_type=None, renderer_context=None):
if 'error' in data.keys():
if 'error' in data:
return data['error']
accessFun = {
"labels" : lambda e: \
",".join(e['labels'].keys()),
",".join(list(e['labels'])),
"dataurl" : lambda e: e['links']['files'],
}
def defaultAccess(e,a):
......@@ -310,9 +295,10 @@ class TSVRenderer(BaseRenderer):
header = "#" + "\t".join(columns)
outTable = [header]
for e in data['events']:
row = [ accessFun.get(column, lambda e: defaultAccess(e,column))(e) for column in columns ]
outTable.append("\t".join(row))
if 'events' in data:
for e in data['events']:
row = [ accessFun.get(column, lambda e: defaultAccess(e,column))(e) for column in columns ]
outTable.append("\t".join(row))
outTable = "\n".join(outTable)
return outTable
......@@ -348,27 +334,40 @@ class EventList(InheritPermissionsAPIView):
`curl -X POST -F "group=Test" -F "type=LM" -F "eventFile=@coinc.xml" --insecure --cert $X509_USER_PROXY https://gracedb.ligo.org/api/events/`
"""
#model = Event
#serializer_class = EventSerializer
model = Event
permission_classes = (IsAuthenticated,IsAuthorizedForPipeline)
parser_classes = (parsers.MultiPartParser,)
renderer_classes = (JSONRenderer, BrowsableAPIRenderer, LigoLwRenderer, TSVRenderer,)
throttle_classes = (BurstAnonRateThrottle, EventCreationThrottle,)
paginator = CustomEventPagination()
def get(self, request, *args, **kwargs):
"""I am the GET docstring for EventList"""
query = request.query_params.get("query")
count = request.query_params.get("count", PAGINATE_BY)
start = request.query_params.get("start", 0)
sort = request.query_params.get("sort", "-created")
columns = request.query_params.get("columns", "")
events = Event.objects
# Start with the base events queryset, all events in the db
# with a valid graceid:
events = Event.objects.filter(graceid__isnull=False)
# FIXME 20240923: ligolw rendering has been completely broken
# since the switch from glue. That hasn't stopped some random
# processes from requesting /api/events/ in the browser and throwing
# up errors. Return a 400 for now, and revisit fixing this later
# on (HA, sure).
if request.accepted_renderer.format == 'xml':
return HttpResponseBadRequest(xml_err_msg)
# Check if this is an external request, only hit the db once:
request_is_external = is_external(request.user)
if query:
# If the user is external, we must check to make sure that any query on FAR
# value is within the safe range.
if is_external(request.user):
if request_is_external:
try:
check_query_far_range(parseQuery(query))
except BadFARRange:
......@@ -378,7 +377,7 @@ class EventList(InheritPermissionsAPIView):
except ParseException:
d = {'error': 'Invalid query' }
return Response(d,status=status.HTTP_400_BAD_REQUEST)
except Exception, e:
except Exception as e:
d = {'error': str(e) }
return Response(d,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
form = SimpleSearchForm(request.GET)
......@@ -390,116 +389,97 @@ class EventList(InheritPermissionsAPIView):
events = filter_events_for_user(events, request.user, 'view')
events = events.order_by(sort).select_subclasses()
start = int(start)
count = int(count)
numRows = events.count()
# Fail if the output format is ligolw, and there are more than 1000 events
if request.accepted_renderer.format == 'xml' and numRows > 1000:
d = {'error': 'Too many events.' }
try:
events = events.order_by(sort).select_subclasses()
except FieldError as e:
d = {'error': str(e) }
return Response(d, status=status.HTTP_400_BAD_REQUEST)
last = max(0, (numRows / count)) * count
rv = {}
links = {}
rv['links'] = links
rv['events'] = [eventToDict(e, request=request)
for e in events[start:start+count]]
baseuri = api_reverse('events:event-list', request=request)
links['self'] = request.build_absolute_uri()
# For some reason, the query and filtering process broke the
# select_related and prefetch_related, so do it now right before
# paginating the result. I think that's best practice anyway?
events = events.select_related(*event_related_objects) \
.prefetch_related(*event_prefetch_objects)
d = { 'start' : 0, "count": count, "sort": sort }
if query: d['query'] = query
links['first'] = baseuri + "?" + urllib.urlencode(d)
d['start'] = last
links['last'] = baseuri + "?" + urllib.urlencode(d)
if start != last:
d['start'] = start+count
links['next'] = baseuri + "?" + urllib.urlencode(d)
rv['numRows'] = events.count()
response = Response(rv)
# XXX Next, we try finalizing and rendering the response. According to
# the django rest framework docs (see .render() in
# http://django-rest-framework.org/api-guide/responses.html), this is
# unusual. But we want to handle the exceptions raised during rendering
# ourselves. And that is not easy to do if the exceptions are raised
# somewhere deep inside the entrails of django.
# NOTE: This will not result in two calls to render(). Django will check
# the _is_rendered property of the response before attempteding to render.
# I have tested this by putting logging commands inside the custom renderers.
try:
if request.accepted_renderer.format == 'xml':
response['Content-Disposition'] = 'attachment; filename=gracedb-query.xml'
# XXX Get the columns into renderer_context. Bizarre? Why, yes.
setattr(self, 'kwargs', {'columns': columns})
# NOTE When finalize_response is calld in its natural habitat, the
# args and kwargs are the same as those passed to the 'handler', i.e.,
# the function we are presently inside.
response = self.finalize_response(request, response, *args, **kwargs)
response.render()
except Exception, e:
try:
status_code = e.status_code
except:
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return Response({'error': str(e)}, status=status_code)
# Set up paginated reply:
serializer_context = {'request': request,
'request_is_external': request_is_external}
paginated_results = self.paginator.paginate_queryset(events, request)
serializer = EventSerializer(paginated_results, many=True,
context=serializer_context)
return self.paginator.get_paginated_response(serializer.data)
return response
#@pipeline_auth_required
def post(self, request, format=None):
rv = {}
rv['warnings'] = []
# Check user authorization for pipeline.
group_name = request.data.get('group', None)
if not group_name=='Test':
try:
pipeline = Pipeline.objects.get(name=request.data['pipeline'])
except:
return Response({'error': "Please provide a valid pipeline."},
status = status.HTTP_400_BAD_REQUEST)
if not user_has_perm(request.user, "populate", pipeline):
return HttpResponseForbidden("You don't have permission on this pipeline.")
# The following looks a bit funny but it is actually necessary. The
# django form expects a dict containing the POST data as the first
# arg, and a dict containing the FILE data as the second. In the
# django-restframework, however, both are in request.data
# TP (21 Nov 2017): Hack to allow basic event submission without
# labels to function with versions of gracedb-client before 1.26.
# Should be removed eventually.
if (request.data.has_key('labels') and request.data['labels'] == ''):
request.data.pop('labels', None)
form = CreateEventForm(request.data, request.data)
if form.is_valid():
event, warnings = _createEventFromForm(request, form)
if event:
rv.update(eventToDict(event, request=request))
rv['warnings'] += warnings
response = Response(rv, status=status.HTTP_201_CREATED)
response["Location"] = api_reverse(
'events:event-detail',
args=[event.graceid],
request=request)
return response
else: # no event created
return Response({'warnings':warnings},
status=status.HTTP_400_BAD_REQUEST)
else: # form not valid
rv = {}
rv['errors'] = ["%s: %s" % (key, form.errors[key].as_text())
for key in form.errors]
return Response(rv, status=status.HTTP_400_BAD_REQUEST)
pipeline_name = request.data['pipeline']
# AEP (May 2020): begin to depreciate pipelines that are no longer
# maintained or supported. Not sure this conditional is the best way
# to do it, but it's a start.
if pipeline_name not in settings.DEPRECIATED_PIPELINES + \
settings.UNAPPROVED_PIPELINES:
# Check user authorization for pipeline.
group_name = request.data.get('group', None)
if not group_name=='Test':
try:
pipeline = Pipeline.objects.get(name=pipeline_name)
except:
return Response({'error': "Please provide a valid pipeline."},
status = status.HTTP_400_BAD_REQUEST)
if not user_has_perm(request.user, "populate", pipeline):
return HttpResponseForbidden("You don't have permission on this pipeline.")
# Get search since we won't block MDC event submissions even if the
# pipeline is disabled
search_name = request.data.get('search', None)
if not pipeline.enabled and search_name != 'MDC':
err_msg = ('The {0} pipeline has been temporarily disabled by '
'an EM advocate due to suspected misbehavior.').format(
pipeline.name)
return HttpResponseBadRequest(err_msg)
# The following looks a bit funny but it is actually necessary. The
# django form expects a dict containing the POST data as the first
# arg, and a dict containing the FILE data as the second. In the
# django-restframework, however, both are in request.data
# TP (21 Nov 2017): Hack to allow basic event submission without
# labels to function with versions of gracedb-client before 1.26.
# Should be removed eventually.
if (request.data.get('labels') == ''):
request.data.pop('labels', None)
form = CreateEventForm(request.data, request.data)
if form.is_valid():
event, warnings = _createEventFromForm(request, form)
if event:
rv.update(eventToDict(event, request=request))
rv['warnings'] += warnings
response = Response(rv, status=status.HTTP_201_CREATED)
response["Location"] = api_reverse(
'events:event-detail',
args=[event.graceid],
request=request)
return response
else: # no event created
return Response({'warnings':warnings},
status=status.HTTP_400_BAD_REQUEST)
else: # form not valid
rv = {}
rv['errors'] = ["%s: %s" % (key, form.errors[key].as_text())
for key in form.errors]
return Response(rv, status=status.HTTP_400_BAD_REQUEST)
elif pipeline_name in settings.DEPRECIATED_PIPELINES:
err_msg =("The %s pipeline is no longer supported in GraceDB. Please "
"contact an administrator to re-enable support." % pipeline_name)
return HttpResponseBadRequest(err_msg)
elif pipeline_name in settings.UNAPPROVED_PIPELINES:
err_msg =("The %s pipeline is not approved to upload to GraceDB. "
"Contact an administrator to enable uploads" % pipeline_name)
return HttpResponseBadRequest(err_msg)
class RawdataParser(parsers.BaseParser):
......@@ -525,7 +505,7 @@ class LigoLwParser(parsers.MultiPartParser):
class EventDetail(InheritPermissionsAPIView):
#parser_classes = (LigoLwParser, RawdataParser)
parser_classes = (parsers.MultiPartParser,)
#serializer_class = EventSerializer
serializer_class = EventSerializer
permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
renderer_classes = (JSONRenderer, BrowsableAPIRenderer, LigoLwRenderer,)
......@@ -533,11 +513,18 @@ class EventDetail(InheritPermissionsAPIView):
@event_and_auth_required
def get(self, request, event):
#response = Response(self.serializer_class(event, context={'request': request}).data)
response = Response(eventToDict(event, request=request))
response = Response(self.serializer_class(event, context={'request': request}).data)
response["Cache-Control"] = "no-cache"
# FIXME 20240923: ligolw rendering has been completely broken
# since the switch from glue. That hasn't stopped some random
# processes from requesting /api/events/ in the browser and throwing
# up errors. Return a 400 for now, and revisit fixing this later
# on (HA, sure).
if request.accepted_renderer.format == 'xml':
return HttpResponseBadRequest(xml_err_msg)
# XXX Next, we try finalizing and rendering the response. According to
# the django rest framework docs (see .render() in
# http://django-rest-framework.org/api-guide/responses.html), this is
......@@ -555,7 +542,7 @@ class EventDetail(InheritPermissionsAPIView):
# the function we are presently inside.
response = self.finalize_response(request, response)
response.render()
except Exception, e:
except Exception as e:
try:
status_code = e.status_code
except:
......@@ -573,9 +560,13 @@ class EventDetail(InheritPermissionsAPIView):
if request.user != event.submitter:
msg = "You (%s) Them (%s)" % (request.user, event.submitter)
return HttpResponseForbidden("You did not create this event. %s" %msg)
except Exception, e:
except Exception as e:
return Response(str(e))
# Compile far and nscand for alerts
old_far = event.far
old_nscand = event.is_ns_candidate()
# messages = []
# if event.group.name != request.data['group']:
# messages += [
......@@ -591,38 +582,112 @@ class EventDetail(InheritPermissionsAPIView):
# return Response("\n".join(messages),
# status=status.HTTP_400_BAD_REQUEST)
# XXX handle duplicate file names.
# Create versioned file
f = request.data['eventFile']
uploadDestination = os.path.join(event.datadir, f.name)
fdest = VersionedFile(uploadDestination, 'w')
#for chunk in f.chunks():
# fdest.write(chunk)
#fdest.close()
shutil.copyfileobj(f, fdest)
fdest.close()
version = create_versioned_file(f.name, event.datadir, f)
# Extract Info from uploaded data
try:
handle_uploaded_data(event, uploadDestination)
event.submitter = request.user
except:
# XXX Bad news. If the log file fails to save because of
# race conditions, then this will also be the the message
# returned. Somehow, I think there are other things that
# could go wrong inside handle_uploaded_data besides just
# bad data. We should probably check for different types
# of exceptions here.
return Response("Bad Data",
status=status.HTTP_400_BAD_REQUEST)
uploadDestination = os.path.join(event.datadir, f.name)
handle_uploaded_data(event, uploadDestination, file_version=version)
event.submitter = request.user
# Save event
event.save()
# Issue alert
EventAlertIssuer(event, alert_type='update').issue_alerts()
EventAlertIssuer(event, alert_type='update').issue_alerts(
old_far=old_far, old_nscand=old_nscand)
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):
cleaned_data = {}
for k, v in data.items():
if k in ['t90', 'redshift', 'ra', 'dec', 'error_radius']:
try:
cleaned_data[k] = float(v)
except ValueError as e:
err_msg = "Parameter '{k}' must be a float".format(k=k)
raise DrfValidationError(err_msg)
elif k == 'designation':
try:
cleaned_data[k] = str(v)
except ValueError as e:
err_msg = "Parameter '{k}' must be a string".format(k=k)
raise DrfValidationError(err_msg)
return cleaned_data
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.pipeline.name not in settings.GRB_PIPELINES
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(
old_far=grbevent.far, old_nscand=grbevent.is_ns_candidate())
return Response(eventToDict(grbevent, request=request))
#==================================================================
# Neighbors
......@@ -641,13 +706,13 @@ class EventNeighbors(InheritPermissionsAPIView):
# and TSV renderers.
@event_and_auth_required
def get(self, request, event):
if request.query_params.has_key('neighborhood'):
if 'neighborhood' in request.query_params:
delta = request.query_params['neighborhood']
try:
if delta.find(',') < 0:
neighborhood = (int(delta), int(delta))
else:
neighborhood = map(int, delta.split(','))
neighborhood = list(map(int, delta.split(',')))
except ValueError:
pass
else:
......@@ -679,7 +744,7 @@ class EventLabel(InheritPermissionsAPIView):
permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
@event_and_auth_required
def get(self, request, event, label):
def get(self, request, event, label=None):
if label is not None:
theLabel = event.labelling_set.filter(label__name=label).all()
if len(theLabel) < 1:
......@@ -689,7 +754,7 @@ class EventLabel(InheritPermissionsAPIView):
return Response(labelToDict(theLabel, request=request))
else:
labels = [ labelToDict(x,request=request)
for x in event.labelling_set.all() ]
for x in event.labelling_set.all().select_related() ]
return Response({
'links' : [{
'self': request.build_absolute_uri(),
......@@ -705,7 +770,7 @@ class EventLabel(InheritPermissionsAPIView):
try:
rv, label_created = create_label(event, request, label)
except (ValueError, Label.ProtectedLabelError) as e:
return Response(e.message,
return Response(str(e),
status=status.HTTP_400_BAD_REQUEST)
# Return response and status code
......@@ -716,11 +781,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(str(e), status=status.HTTP_404_NOT_FOUND)
except (ValueError, Label.ProtectedLabelError) as e:
return Response(e.message,
status=status.HTTP_400_BAD_REQUEST)
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
......@@ -764,11 +831,13 @@ class EventLogList(InheritPermissionsAPIView):
@event_and_auth_required
def post(self, request, event):
message = request.data.get('comment')
label = request.data.get('label', None)
# Handle requests encoded as multipart/form or regular JSONs
if isinstance(request.data, QueryDict):
# request.data is a MultiValueDict
tagnames = request.data.getlist('tagname', [])
displayNames = request.data.getlist('displayName', [])
#label = request.data.getlist('label', None)
else:
# request.data is a normal dict
tagnames = request.data.get('tagname', [])
......@@ -784,17 +853,12 @@ class EventLogList(InheritPermissionsAPIView):
file_version = None
if uploadedFile:
filename = uploadedFile.name
filepath = os.path.join(event.datadir, filename)
try:
# Open / Write the file.
fdest = VersionedFile(filepath, 'w')
for chunk in uploadedFile.chunks():
fdest.write(chunk)
fdest.close()
# Ascertain the version assigned to this particular file.
file_version = fdest.version
except Exception, e:
file_version = create_versioned_file(filename, event.datadir,
uploadedFile)
except Exception as e:
# XXX This needs some thought.
response = Response(str(e), status=status.HTTP_400_BAD_REQUEST)
......@@ -823,10 +887,23 @@ class EventLogList(InheritPermissionsAPIView):
use_display_names = True
else:
use_display_names = False
request.data['displayName'] = ''
#https://stackoverflow.com/questions/52367379/why-is-django-rest-frameworks-request-data-sometimes-immutable
try:
request.data['displayName'] = ''
except AttributeError:
mutable = request.data._mutable # save state
request.data._mutable = True # make mutable
request.data['displayName'] = ''
request.data._mutable = mutable # return to original state
tw_dict = {}
if tagnames and len(tagnames):
try:
mutable = request.data._mutable # save state
request.data._mutable = True # make mutable
except AttributeError:
pass
for i,tagname in enumerate(tagnames):
n = logentry.N
......@@ -847,12 +924,20 @@ class EventLogList(InheritPermissionsAPIView):
rv = eventLogToDict(logentry, request=request)
response = Response(rv, status=status.HTTP_201_CREATED)
response['Location'] = rv['self']
#if 'tagWarning' in tw_dict.keys():
#if 'tagWarning' in tw_dict:
# response['tagWarning'] = tw_dict['tagWarning']
# Issue alert.
EventLogAlertIssuer(logentry, alert_type='log').issue_alerts()
# Now apply labels
if label:
try:
rv, label_created = create_label(event, request, label)
except (ValueError, Label.ProtectedLabelError) as e:
return Response(str(e),
status=status.HTTP_400_BAD_REQUEST)
return response
class EventLogDetail(InheritPermissionsAPIView):
......@@ -906,12 +991,12 @@ class EMBBEventLogList(InheritPermissionsAPIView):
try:
# Alert is issued in this code
eel = create_eel(request.data, event, request.user)
except ValueError, e:
except ValueError as e:
return Response("%s" % str(e), status=status.HTTP_400_BAD_REQUEST)
except IntegrityError, e:
except IntegrityError as e:
return Response("Failed to save EMBB entry: %s" % str(e),
status=status.HTTP_503_SERVICE_UNAVAILABLE)
except Exception, e:
except Exception as e:
return Response("Problem creating EEL: %s" % str(e),
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
......@@ -954,7 +1039,7 @@ class EMObservationList(InheritPermissionsAPIView):
# XXX Note the following hack.
# If this JSON information is requested for skymapViewer, use a different
# representation for backwards compatibility.
if 'skymapViewer' in request.query_params.keys():
if 'skymapViewer' in request.query_params:
emo = [ skymapViewerEMObservationToDict(emo, request)
for emo in emo_set.iterator() ]
......@@ -990,12 +1075,12 @@ class EMObservationList(InheritPermissionsAPIView):
try:
# Create EMObservation - alert is issued inside this code
emo = create_emobservation(request, event)
except ValueError, e:
except ValueError as e:
return Response("%s" % str(e), status=status.HTTP_400_BAD_REQUEST)
except IntegrityError, e:
except IntegrityError as e:
return Response("Failed to save EMBB observation record: %s" % str(e),
status=status.HTTP_503_SERVICE_UNAVAILABLE)
except Exception, e:
except Exception as e:
return Response("Problem creating EMBB Observation: %s" % str(e),
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
......@@ -1170,7 +1255,7 @@ class EventLogTagDetail(InheritPermissionsAPIView):
# client that the creation was sucessful when, in fact, the database
# was unchanged.
tag = eventlog.tags.filter(name=tagname)[0]
msg = "Log already has tag %s" % unicode(tag)
msg = "Log already has tag {0}".format(tag.name)
return Response(msg,status=status.HTTP_409_CONFLICT)
except:
# Check authorization
......@@ -1237,7 +1322,7 @@ class EventLogTagDetail(InheritPermissionsAPIView):
return Response("Tag removed, but failed to create log entry: %s" % str(e),
status=status.HTTP_200_OK)
return Response("Tag deleted.",status=status.HTTP_200_OK)
return Response("Tag deleted.",status=status.HTTP_204_NO_CONTENT)
except:
return Response("Tag not found.",status=status.HTTP_404_NOT_FOUND)
......@@ -1355,7 +1440,7 @@ class GroupEventPermissionDetail(InheritPermissionsAPIView):
permission=underlying_permission)
underlying_event.refresh_perms()
except Exception, e:
except Exception as e:
# We're gonna blame the user here.
return Response("Problem creating permission: %" % str(e),
status=status.HTTP_400_BAD_REQUEST)
......@@ -1421,7 +1506,7 @@ class GroupEventPermissionDetail(InheritPermissionsAPIView):
except GroupObjectPermission.DoesNotExist:
return Response("GroupObjectPermission not found.",
status=status.HTTP_404_NOT_FOUND)
except Exception, e:
except Exception as e:
return Response("Problem deleting permission: %s" % str(e),
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
......@@ -1503,25 +1588,20 @@ class Files(InheritPermissionsAPIView):
def put(self, request, event, filename=""):
""" File uploader. Implements file versioning. """
filename = filename or ""
filepath = os.path.join(event.datadir, filename)
try:
# Open / Write the file.
fdest = VersionedFile(filepath, 'w')
f = request.data['upload']
for chunk in f.chunks():
fdest.write(chunk)
fdest.close()
file_version = fdest.version
file_version = create_versioned_file(filename, event.datadir, f)
rv = {}
# XXX this seems wobbly.
longname = fdest.name
longname = os.path.join(event.datadir, filename)
shortname = longname[longname.rfind(filename):]
rv['permalink'] = api_reverse(
"events:files", args=[event.graceid, shortname], request=request)
response = Response(rv, status=status.HTTP_201_CREATED)
except Exception, e:
except Exception as e:
# XXX This needs some thought.
response = Response(str(e), status=status.HTTP_400_BAD_REQUEST)
# XXX Uhm, we don't to try creating a log message for this, right?
......@@ -1588,16 +1668,11 @@ class VOEventList(InheritPermissionsAPIView):
@event_and_auth_required
def post(self, request, event):
# Get data from request
voevent_type = request.data.get('voevent_type', None)
if not voevent_type:
msg = "You must provide a valid voevent_type."
return Response({'error': msg}, status = status.HTTP_400_BAD_REQUEST)
internal = request.data.get('internal', 1)
skymap_type = request.data.get('skymap_type', None)
skymap_filename = request.data.get('skymap_filename', None)
open_alert = request.data.get('open_alert', 0)
hardware_inj = request.data.get('hardware_inj', 0)
CoincComment = request.data.get('CoincComment', None)
......@@ -1607,41 +1682,104 @@ class VOEventList(InheritPermissionsAPIView):
NSBH = request.data.get('NSBH', None)
BBH = request.data.get('BBH', None)
Terrestrial = request.data.get('Terrestrial', None)
HasMassGap = request.data.get('HasMassGap', None)
Significant = request.data.get('Significant', 0)
# old parameter included to warn users:
MassGap = request.data.get('MassGap', None)
if (skymap_filename and not skymap_type) or (skymap_type and not skymap_filename):
msg = "Both or neither of skymap_time and skymap_filename must be specified."
return Response({'error': msg}, status = status.HTTP_400_BAD_REQUEST)
# Get RAVEN data
ext_gcn = request.data.get('ext_gcn', None)
ext_pipeline = request.data.get('ext_pipeline', None)
ext_search = request.data.get('ext_search', None)
time_coinc_far = request.data.get('time_coinc_far', None)
space_coinc_far = request.data.get('space_coinc_far', None)
combined_skymap_filename = request.data.get('combined_skymap_filename',
None)
delta_t = request.data.get('delta_t', None)
raven_coinc = request.data.get('raven_coinc', None)
# Get VOEvent types as a dict (key = short form, value = long form)
VOEVENT_TYPE_DICT = dict(VOEvent.VOEVENT_TYPE_CHOICES)
# Check data
error = False
if not voevent_type or voevent_type not in VOEVENT_TYPE_DICT:
error = True
msg = "You must provide a valid voevent_type."
elif ((skymap_filename and not skymap_type) or
(skymap_type and not skymap_filename)):
error = True
msg = ("Both or neither of skymap_type and skymap_filename must "
"be specified.")
elif not event.gpstime:
error = True
msg = "Cannot build a VOEvent because event has no gpstime."
elif not event.far:
error = True
msg = "Cannot build a VOEvent because event has no FAR."
elif (voevent_type in ["IN", "UP"] or
voevent_type == "PR" and skymap_filename is not None):
if skymap_filename is None:
error = True
msg = "Skymap filename not provided."
if skymap_type is None:
error = True
msg = "Skymap type must be provided."
# Check if skymap file exists
skymap_file_path = os.path.join(event.datadir, skymap_filename)
if not os.path.exists(skymap_file_path):
error = True
msg = "Skymap file {fname} does not exist".format(
fname=skymap_filename)
elif time_coinc_far or space_coinc_far:
if not ext_gcn:
error = True
msg = "External GCN ID not provided"
elif not ext_pipeline:
error = True
msg = "External Pipeline not provided"
elif not ext_search:
error = True
msg = "External Search not provided"
elif MassGap:
error = True
msg = "MassGap has been replaced by HasMassGap"
# If there's an error, return a 400 response
if error:
return Response({'error': msg}, status=status.HTTP_400_BAD_REQUEST)
# Instantiate the voevent and save in order to get the serial number
voevent = VOEvent(voevent_type=voevent_type, event=event, issuer=request.user)
voevent = VOEvent(event=event, issuer=request.user,
voevent_type=voevent_type, skymap_type=skymap_type,
skymap_filename=skymap_filename, internal=internal,
hardware_inj=hardware_inj, coinc_comment=CoincComment,
prob_has_ns=ProbHasNS, prob_has_remnant=ProbHasRemnant,
prob_bns=BNS, prob_nsbh=NSBH, prob_bbh=BBH,
prob_terrestrial=Terrestrial, prob_has_mass_gap=HasMassGap,
significant=Significant)
try:
voevent.save()
except ValidationError as e:
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response("Failed to create VOEvent: %s" % str(e),
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# Now, you need to actually build the VOEvent.
try:
voevent_text, ivorn = buildVOEvent(event, voevent.N, voevent_type, request,
skymap_filename = skymap_filename, skymap_type = skymap_type,
internal = internal, open_alert=open_alert,
hardware_inj=hardware_inj, CoincComment=CoincComment,
ProbHasNS=ProbHasNS, ProbHasRemnant=ProbHasRemnant, BNS=BNS,
NSBH=NSBH, BBH=BBH, Terrestrial=Terrestrial, MassGap=MassGap)
except VOEventBuilderException, e:
msg = "Problem building VOEvent: %s" % str(e)
return Response({'error': msg}, status = status.HTTP_400_BAD_REQUEST)
voevent_display_type = dict(VOEvent.VOEVENT_TYPE_CHOICES)[voevent_type].capitalize()
voevent_text, ivorn = construct_voevent_file(event, voevent,
request=request)
if VOEVENT_TYPE_DICT[voevent_type] == 'earlywarning':
voevent_display_type = 'EarlyWarning'
else:
voevent_display_type = VOEVENT_TYPE_DICT[voevent_type].capitalize()
filename = "%s-%d-%s.xml" % (event.graceid, voevent.N, voevent_display_type)
filepath = os.path.join(event.datadir, filename)
fdest = VersionedFile(filepath, 'w')
fdest.write(voevent_text)
fdest.close()
file_version = fdest.version
file_version = create_versioned_file(filename, event.datadir,
voevent_text)
voevent.filename = filename
voevent.file_version = file_version
......
from __future__ import absolute_import
import decimal
from decimal import InvalidOperation
import logging
from django.utils import six
import six
from rest_framework import exceptions, fields
......@@ -12,15 +12,13 @@ logger = logging.getLogger(__name__)
class CustomHiddenDefault(fields.CurrentUserDefault):
context_key = None
requires_context = True
def __init__(self, *args, **kwargs):
self.context_key = kwargs.pop('context_key', None)
def set_context(self, serializer_field):
self.custom_field = self.get_field_value(serializer_field)
def __call__(self, context_key=None):
return self.custom_field
def __call__(self, serializer_field):
return self.get_field_value(serializer_field)
def get_field_value(self, serializer_field):
# Derived classes will probably want to override this
......@@ -121,10 +119,14 @@ class GenericField(fields.Field):
try:
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])
raise exceptions.ValidationError(error_msg)
if hasattr(self, 'get_does_not_exist_error'):
err_msg = self.get_does_not_exist_error(data)
else:
err_msg = '{model} with {lf}={data} does not exist'.format(
model=self.model.__name__, lf=list(model_dict)[0],
data=list(model_dict.values())[0]
)
raise exceptions.ValidationError(err_msg)
def get_model_dict(self, data):
return {self.lookup_field: data}
......@@ -136,9 +138,33 @@ class CustomDecimalField(fields.DecimalField):
# Proper handling of floats: convert to quantized decimal.Decimal
# and then back to a string, so that the rest of the processing
# can continue
if isinstance(data, float):
data = decimal.Decimal(data).quantize(
decimal.Decimal(10)**(-1 * self.decimal_places))
data = data.to_eng_string()
# AEP Update: This required some tweaking. When requests are made
# as application/json, then the resulting 'request.data' is a dict,
# and the values within the raw dict are what they should be (floats,
# etc.) BUT, for form-encoded data, the request.data a QueryDict object
# and the containing fields are strings. So what was happening if the
# POSTed */form/* fields had too many decimal places was, the conditional
# wouuldn't recognize it as a float, then it would pass the raw string
# back to the serializer, which would fail when it tried to insert it into
# the database (which was expecting a max number of decimal places"
# What is does now is, it will accept either a float or a string, but if the
# decimal conversion fails because you're not inputting a valid number, then
# it just passes it right along and lets the serializer's error checking
# return an error to the user.
# I confirmed that it has the same behavior for json requests (i.e., db
# modification for numbers, and a 400 "A valid number is reuqired" otherwise)
# *and* for form-encoded requests. So changing t_start on a superevent
# to 'apple' fails gracefully.
if isinstance(data, float) or isinstance(data, str):
try:
data = decimal.Decimal(data).quantize(
decimal.Decimal(10)**(-1 * self.decimal_places))
data = data.to_eng_string()
except InvalidOperation:
pass
return super(CustomDecimalField, self).to_internal_value(data)
import logging
from rest_framework import filters
from rest_framework_guardian import filters
# Set up logger
logger = logging.getLogger(__name__)
class DjangoObjectAndGlobalPermissionsFilter(
filters.DjangoObjectPermissionsFilter):
filters.ObjectPermissionsFilter):
"""
Same as DjangoObjectPermissionsFilter, except it allows global permissions.
Same as ObjectPermissionsFilter, except it allows global permissions.
"""
accept_global_perms = True
def filter_queryset(self, request, queryset, view):
# Mostly from rest_framework.filters.DjangoObjectPermissionsFilter
#
# We want to defer this import until run-time, rather than import-time.
# See https://github.com/encode/django-rest-framework/issues/4608
# (Also see #1624 for why we need to make this import explicitly)
from guardian.shortcuts import get_objects_for_user
extra = {}
user = request.user
model_cls = queryset.model
kwargs = {
'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.model_name
}
permission = self.perm_format % kwargs
extra['accept_global_perms'] = self.accept_global_perms
return get_objects_for_user(user, permission, queryset, **extra)
shortcut_kwargs = filters.ObjectPermissionsFilter.shortcut_kwargs
shortcut_kwargs['accept_global_perms'] = True
import logging
from django.conf import settings
from django.urls import resolve
from rest_framework import permissions
from superevents.models import Superevent, Signoff
from ..permissions import FunctionalModelPermissions, \
FunctionalObjectPermissions, FunctionalParentObjectPermissions
# Set up logger
logger = logging.getLogger(__name__)
class gwtc_model_permissions(FunctionalModelPermissions):
authenticated_users_only = True
allowed_methods = ['GET', 'POST']
def get_post_permissions(self, request):
# Just return the 'add_gwtc_catalog' perm for now until there
# needs to be finer-grained control or new methods.
return ['gwtc.add_gwtc_catalog']
from __future__ import absolute_import
import functools
import logging
import os
import json
import numbers
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework import fields, serializers, validators
from rest_framework.exceptions import ValidationError
from gwtc.models import gwtc_catalog, gwtc_gevent, gwtc_superevent
from superevents.models import Superevent
from events.models import Event, Pipeline
# Set up user model
UserModel = get_user_model()
# Set up logger
logger = logging.getLogger(__name__)
# some variables:
PIPELINE_KEY = 'pipelines'
class gwtc_serializer(serializers.ModelSerializer):
number = serializers.SlugField(required=True, allow_null=False, allow_blank=False)
smap = serializers.JSONField(required=True, write_only=True, allow_null=False)
version = serializers.ReadOnlyField()
comment = serializers.CharField(allow_blank=True, allow_null=True, required=False)
# Get user from request automatically
user = serializers.HiddenField(write_only=True,
default=serializers.CurrentUserDefault())
submitter = serializers.SlugRelatedField(slug_field='username',
read_only=True)
created = serializers.DateTimeField(format=settings.GRACE_STRFTIME_FORMAT,
read_only=True)
# Custom display fields. These are read-only fields for displaying/GET'ing
# with the API:
gwtc_superevents = serializers.SerializerMethodField(read_only=True)
class Meta:
model = gwtc_catalog
fields = ('number', 'version', 'created', 'submitter',
'smap', 'user', 'gwtc_superevents', 'comment')
# Make sure the comment is a blank and not null:
def validate_comment(self, value):
if not value:
return ''
else:
return value
# Look at the smap json. Perform checks such as verifying that superevents
# are in the database, and that events are part of that superevent.
def validate_smap(self, value):
for sevent, sevent_dict in value.items():
# first try and get the superevent:
try:
s = Superevent.get_by_date_id(sevent)
except (ObjectDoesNotExist, Superevent.DateIdError):
raise serializers.ValidationError(f'Superevent {sevent} '
'was not found in the database.')
# validate the per-pipeline events dictionary:
try:
events_dict = sevent_dict[PIPELINE_KEY]
if isinstance(events_dict, dict):
# Now loop over the events and pipelines that are part of that superevent:
for pipeline, event in events_dict.items():
# see if the event exists:
try:
e = Event.getByGraceid(event)
# ensure that we have the base event class:
if hasattr(e, 'event_ptr'):
e = e.event_ptr
except ObjectDoesNotExist:
raise serializers.ValidationError(f'Catalog Error: Event {event} '
'was not found in the database.')
except Exception:
raise serializers.ValidationError(f'Unable to parse event {event}')
# now, see if the event actually is part of the superevent:
if not e in s.events.all():
raise serializers.ValidationError(f'Catalog Error: Event {event} '
f'is not part of superevent {sevent}.')
# now see if the event's pipeline matches up with what is provided.
if not e.pipeline.name == pipeline:
raise serializers.ValidationError('Catalog Error: '
f'Pipeline of {event} ({e.pipeline.name}) '
f'does not match the supplied pipeline ({pipeline})')
else:
raise serializers.ValidationError('value corresponding to '
f'{sevent}["{PIPELINE_KEY}"] is not a dictionary')
except KeyError:
raise serializers.ValidationError('superevent key '
f'{PIPELINE_KEY} not found for gwtc_superevent '
f'{sevent}.')
# test far:
try:
far = sevent_dict['far']
if far and not isinstance(far, numbers.Number):
raise serializers.ValidationError('far value for '
f'{sevent} must be null or a valid number, not '
f'{far}')
except KeyError:
raise serializers.ValidationError('far not found for '
f'superevent {sevent}')
# test pastro:
try:
pastro = sevent_dict['pastro']
if pastro and not isinstance(pastro, dict):
raise serializers.ValidationError('pastro value for '
f'{sevent} must be null or a valid dictionary, not '
f'{pastro}')
except KeyError:
raise serializers.ValidationError('pastro not found for '
f'superevent {sevent}')
# I think that's good? so return the validated data
return value
def validate(self, data):
data = super(gwtc_serializer, self).validate(data)
return data
def create(self, validated_data):
# This is the routine that creates the catalog object, the catalog superevents,
# and catalog events.
# First, create the catalog object:
new_gwtc = gwtc_catalog.objects.create(number=validated_data.pop('number'),
submitter=validated_data.pop('user'),
comment=validated_data.pop('comment', ''))
# Similar to the validation step, loop over the superevents and events and then
# add them to the catalog.
catalog_map = validated_data.pop('smap')
for sevent, sevent_dict in catalog_map.items():
# Get the superevent, create the gwtc superevent
s = Superevent.get_by_date_id(sevent)
new_gwtc_superevent = gwtc_superevent.objects.create(
superevent = s,
gwtc_catalog = new_gwtc,
far=sevent_dict['far'],
pastro=sevent_dict['pastro'])
# Get the per-pipeline events:
pipeline_events = sevent_dict.pop(PIPELINE_KEY)
# Now create the events. It would be more efficient to pre-fetch the pipelines,
# but whatever.
for pipeline, event in pipeline_events.items():
new_gwtc_event = gwtc_gevent.objects.create(
gwtc_catalog = new_gwtc,
gwtc_superevent = new_gwtc_superevent,
gevent = Event.getByGraceid(event),
pipeline = Pipeline.objects.get(name=pipeline))
return new_gwtc
# Populate custom fields.
def get_gwtc_superevents(self, obj):
return {se.superevent.superevent_id:
{
'pipelines': {e.gevent.pipeline.name: e.gevent.graceid
for e in se.gwtc_gevent_set.all()},
'far': se.far,
'pastro': se.pastro,
}
for se in obj.gwtc_superevent_set.all()}
from __future__ import absolute_import
import datetime
import ipdb
import json
import pytest
from django.conf import settings
from django.urls import reverse
from django.core.cache import cache
from guardian.shortcuts import assign_perm, remove_perm
from api.tests.utils import GraceDbApiTestBase
from core.tests.utils import GraceDbTestBase, \
catalog_managers_group_and_user_setup
from gwtc.tests.mixins import gwtc_create_mixin
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)
class test_gwtc_list(GraceDbApiTestBase, gwtc_create_mixin,
catalog_managers_group_and_user_setup):
@classmethod
def setUpClass(cls):
super(test_gwtc_list, cls).setUpClass()
cls.url = v_reverse('gwtc:gwtc-list')
@classmethod
def setUpTestData(cls):
super(test_gwtc_list, cls).setUpTestData()
# an initial catalog object:
cls.gwtc_test_1 = cls.create_gwtc_catalog(user=cls.internal_user,
gwtc_number='test1')
# the old gwtc-v1 schema
cls.json_upload_old = '{{ "{superevent_id}":{{"{pipeline}":"{graceid}"}} }}'
# a template json string for POST operations, gwtc-v2 schema:
cls.json_upload = '{{ "{superevent_id}": {{ "pipelines": {{"{pipeline}":"{graceid}"}}, "far": {far}, "pastro": {pastro} }} }}'
# two test superevents:
cls.test_superevent1 = cls.create_superevent(cls.internal_user)
cls.test_superevent2 = cls.create_superevent(cls.internal_user)
# a test coincinspiralevent:
cls.test_coinc = cls.create_coinc_event('CBC', 'gstlal')
# Some far and pastro values:
cls.null_value = 'null'
cls.far_num = 1.0e-8
cls.string_value = 'some_string'
cls.pastro_dict = '{"BNS": 1.0}'
cls.pastro_broken = '{"BNS"}'
# smap jsons based on those two test superevents:
cls.smap_superevent1 = cls.json_upload.format(superevent_id = cls.test_superevent1.superevent_id,
pipeline = cls.test_superevent1.preferred_event.pipeline.name,
graceid = cls.test_superevent1.preferred_event.graceid,
far=cls.far_num,
pastro=cls.pastro_dict)
cls.smap_superevent2 = cls.json_upload.format(superevent_id = cls.test_superevent2.superevent_id,
pipeline = cls.test_superevent2.preferred_event.pipeline.name,
graceid = cls.test_superevent2.preferred_event.graceid,
far=cls.far_num,
pastro=cls.pastro_dict)
# json with an invalid superevent ID (date not in range):
cls.smap_bad_sid = cls.json_upload.format(superevent_id = "S123456ab",
pipeline = cls.test_superevent1.preferred_event.pipeline.name,
graceid = cls.test_superevent1.preferred_event.graceid,
far=cls.far_num,
pastro=cls.pastro_dict)
# json with valid superevent id, but it isn't in the database:
cls.smap_no_sevent = cls.json_upload.format(superevent_id = "S231129ab",
pipeline = cls.test_superevent1.preferred_event.pipeline.name,
graceid = cls.test_superevent1.preferred_event.graceid,
far=cls.far_num,
pastro=cls.pastro_dict)
# json with graceid not in the database:
cls.smap_no_gevent = cls.json_upload.format(superevent_id = cls.test_superevent2.superevent_id,
pipeline = cls.test_superevent2.preferred_event.pipeline.name,
graceid = "G12345",
far=cls.far_num,
pastro=cls.pastro_dict)
# json with invalid pipeline name in the upload:
cls.smap_bad_pipeline = cls.json_upload.format(superevent_id = cls.test_superevent2.superevent_id,
pipeline = "gstlol",
graceid = cls.test_superevent2.preferred_event.graceid,
far=cls.far_num,
pastro=cls.pastro_dict)
# json with a valid coinc graceid, but it's not part of the associated superevent:
cls.smap_coinc_gevent = cls.json_upload.format(superevent_id = cls.test_superevent2.superevent_id,
pipeline = cls.test_superevent2.preferred_event.pipeline.name,
graceid = cls.test_coinc.graceid,
far=cls.far_num,
pastro=cls.pastro_dict)
def test_internal_user_get_gwtc_list(self):
"""Internal user sees catalog entries"""
response = self.request_as_user(self.url, "GET", self.internal_user)
# confirm the user can access the catalogs
self.assertEqual(response.status_code, 200)
# confirm there is only one catalog entry:
data = response.data['results']
self.assertEqual(len(data), 1)
# confirm the number is correct:
data = data[0]
gwtc_name = data['number']
self.assertEqual(gwtc_name, 'test1')
# confirm that the version is 1:
gwtc_version = int(data['version'])
self.assertEqual(gwtc_version, 1)
def test_public_gwtc_access(self):
"""At this time, the public cannot see gwtc objects"""
response = self.request_as_user(self.url, "GET")
# confirm the public cannot see gwtc objects
self.assertEqual(response.status_code, 403)
def test_internal_user_not_create_gwtc(self):
"""Authenticated users not in catalog_managers cannot create gwtc's"""
request_data = {"number": "test1", "smap": self.smap_superevent1}
response = self.request_as_user(self.url, "POST", self.internal_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 403)
def test_cm_user_create_gwtc(self):
"""Verify a catalog manager can create new gwtc objects"""
request_data = {"number": "test1", "smap": self.smap_superevent1}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 201)
# get the new list and confirm version numbers, etc:
response = self.request_as_user(self.url, "GET", self.cm_user)
# confirm there are two catalog entries:
data = response.data['results']
self.assertEqual(len(data), 2)
def test_cm_user_create_gwtc_with_comment(self):
"""Verify a catalog manager can create new gwtc objects"""
test_comment = "this is a test catalog"
request_data = {"number": "test1", "smap": self.smap_superevent1, "comment": test_comment}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 201)
# get the new list and confirm version numbers, etc:
response = self.request_as_user(self.url, "GET", self.cm_user)
# confirm there are two catalog entries:
data = response.data['results']
self.assertEqual(len(data), 2)
# confirm that the comment is in the first (latest) catalog:
self.assertEqual(data[0]['comment'], test_comment)
def test_queries_and_paths(self):
"""Make another catalog with a different number, and then verify
that the API query paths work."""
# create a second number='test1' gwtc. this should be version 2:
request_data = {"number": "test1", "smap": self.smap_superevent1}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
self.assertEqual(response.status_code, 201)
# create a number='test2' gwtc:
request_data = {"number": "test2", "smap": self.smap_superevent2}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
self.assertEqual(response.status_code, 201)
# get all the gwtc's and confirm there are three:
response = self.request_as_user(self.url, "GET", self.internal_user)
data = response.data['results']
self.assertEqual(len(data), 3)
# get all 'test1' gwtc's and count there are two:
response = self.request_as_user(self.url+'test1/',
"GET", self.internal_user)
data = response.data['results']
self.assertEqual(len(data), 2)
# get the 'latest' test1 gtwc, verify that it's version=2
response = self.request_as_user(self.url+'test1/latest/',
"GET", self.internal_user)
gwtc_version = int(response.data['version'])
self.assertEqual(gwtc_version, 2)
def test_get_wrong_number(self):
"""Querying for the wrong number gives no results"""
response = self.request_as_user(self.url+'test123/',
"GET", self.internal_user)
# confirm the status code is 200:
self.assertEqual(response.status_code, 200)
# confirm there are no catalog entries:
data = response.data['results']
self.assertEqual(len(data), 0)
def test_create_bad_superevent_id(self):
"""User attempts to upload smap with invalid superevent_id"""
request_data = {"number": "test1", "smap": self.smap_bad_sid}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 400)
def test_create_no_superevent(self):
"""User attempts to upload smap with a superevent_id not in the db"""
request_data = {"number": "test1", "smap": self.smap_no_gevent}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 400)
def test_create_no_gevent(self):
"""User attempts to upload smap with a graceid not in the db"""
request_data = {"number": "test1", "smap": self.smap_no_gevent}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 400)
def test_create_pipeline_mismatch(self):
"""User attempts to upload smap with a mismatch between pipeline
and the g-event"""
request_data = {"number": "test1", "smap": self.smap_bad_pipeline}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 400)
def test_coinc_not_in_superevent(self):
""" User attempts to upload smap with valid graceic, but not part
of the specified superevent """
request_data = {"number": "test1", "smap": self.smap_coinc_gevent}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 400)
def test_create_gwtc_null_far_pastro(self):
"""Verify a catalog manager can create new gwtc objects"""
# construct a new smap
smap_test = self.json_upload.format(superevent_id = self.test_superevent1.superevent_id,
pipeline = self.test_superevent1.preferred_event.pipeline.name,
graceid = self.test_superevent1.preferred_event.graceid,
far=self.null_value,
pastro=self.null_value)
request_data = {"number": "test1", "smap": smap_test}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 201)
def test_create_gwtc_bad_pastro(self):
"""Verify a catalog manager can create new gwtc objects"""
# construct a new smap
smap_test = self.json_upload.format(superevent_id = self.test_superevent1.superevent_id,
pipeline = self.test_superevent1.preferred_event.pipeline.name,
graceid = self.test_superevent1.preferred_event.graceid,
far=self.null_value,
pastro=self.pastro_broken)
request_data = {"number": "test1", "smap": smap_test}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 400)
def test_create_gwtc_old_schema(self):
"""Verify a catalog manager can create new gwtc objects"""
# construct a new smap
smap_test = self.json_upload_old.format(superevent_id = self.test_superevent1.superevent_id,
pipeline = self.test_superevent1.preferred_event.pipeline.name,
graceid = self.test_superevent1.preferred_event.graceid)
request_data = {"number": "test1", "smap": smap_test}
response = self.request_as_user(self.url, "POST", self.cm_user,
data=request_data)
# confirm the status code is correct:
self.assertEqual(response.status_code, 400)
from django.urls import re_path, include, path
from rest_framework import routers
from .viewsets import *
urlpatterns = [
# Listing of all gwtc's:
re_path(r'^$', gwtc_viewset.as_view({'get': 'list', 'post': 'create'}), name='gwtc-list'),
# Listing of all versions of a gwtc number:
path('<slug:number>/', gwtc_viewset.as_view({'get': 'list',}), name='gwtc-number-list'),
# show a single version of a gwtc number:
path('<slug:number>/<slug:version>/', gwtc_viewset.as_view({'get': 'retrieve',}), name='gwtc-version-detail'),
]
import os
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Max
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins, parsers, permissions, serializers, status, \
viewsets, pagination
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView
from ..mixins import SafeCreateMixin, InheritDefaultPermissionsMixin
from gwtc.models import gwtc_catalog, gwtc_gevent, gwtc_superevent
from .serializers import gwtc_serializer
from .permissions import gwtc_model_permissions
class custom_gwtc_paginator(pagination.LimitOffsetPagination):
default_limit = 10
limit_query_param = 'count'
offset_query_param = 'start'
class gwtc_viewset(SafeCreateMixin, InheritDefaultPermissionsMixin,
viewsets.ModelViewSet):
queryset = gwtc_catalog.objects.all()
serializer_class = gwtc_serializer
pagination_class = custom_gwtc_paginator
permission_classes = (gwtc_model_permissions, )
# This function gets called for 'list' methods, and returns a queryset. This
# is called for `GET` requests to /api/gwtc/ and /api/gwtc/{number}
def get_queryset(self):
# filter the kwargs. 'number' might be none, depending on the request
# address, in which case return the original queryset.
selected_number = self.kwargs.get('number')
selected_version = self.kwargs.get('version')
# if the catalog number is specified, then return the filtered queryset
if selected_number:
self.queryset = self.queryset.filter(number=selected_number)
return self.queryset.prefetch_related('gwtc_superevent_set',
'gwtc_superevent_set__superevent',
'gwtc_superevent_set__gwtc_gevent_set',
'gwtc_superevent_set__gwtc_gevent_set__gevent',
'gwtc_superevent_set__gwtc_gevent_set__gevent__pipeline')
# This function gets called for 'list' methods, and returns a queryset. This
# is called for `GET` requests to /api/gwtc/{number}/{version}
def get_object(self):
# Get the kwargs from the request path.
selected_number = self.kwargs.get('number')
selected_version = self.kwargs.get('version')
# a request to 'latest' should return the latest value, so put in a special
# case for that.
if selected_version.lower() == 'latest':
selected_version = \
self.queryset.filter(number=selected_number).aggregate(Max('version'))['version__max']
filter_kwargs = {'number': selected_number, 'version': selected_version}
obj = get_object_or_404(self.queryset, **filter_kwargs)
return obj
......@@ -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')
......@@ -7,7 +7,6 @@ from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group as AuthGroup
from django.http import HttpResponse, HttpResponseForbidden
from django.utils.http import unquote_plus
from rest_framework import parsers, status
from rest_framework.permissions import IsAuthenticated
......@@ -17,6 +16,8 @@ from rest_framework.settings import api_settings
from rest_framework.views import APIView
from rest_framework.generics import RetrieveAPIView
from urllib.parse import unquote_plus
from api.backends import GraceDbX509FullCertAuthentication, \
GraceDbX509CertInfosAuthentication
from api.utils import api_reverse
......@@ -103,6 +104,17 @@ 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}")
gwtc_number_list = api_reverse('gwtc:gwtc-number-list', args=['4a-1'], request=request)
gwtc_number_list = gwtc_number_list.replace('4a-1', '{number}')
gwtc_version_detail = api_reverse('gwtc:gwtc-version-detail', args=['4a-1', '10'], request=request)
gwtc_version_detail = gwtc_version_detail.replace('4a-1', '{number}')
gwtc_version_detail = gwtc_version_detail.replace('10', '{version}')
# XXX Need a template for the tag list?
templates = {
......@@ -119,6 +131,9 @@ class GracedbRoot(APIView):
"tag-template" : tag,
"taglist-template" : taglist,
"signoff-list-template": signofflist,
"update-grbevent-template": update_grbevent,
"gwtc-number-list": gwtc_number_list,
"gwtc-version-detail": gwtc_version_detail,
}
# Get superevent templates
......@@ -130,6 +145,7 @@ class GracedbRoot(APIView):
"superevents" : api_reverse("superevents:superevent-list",
request=request),
"events" : api_reverse("events:event-list", request=request),
"gwtc" : api_reverse("gwtc:gwtc-list", request=request),
"self" : api_reverse("root", request=request),
"performance" : api_reverse("performance-info", request=request),
"user-info": api_reverse("user-info", request=request),
......@@ -150,6 +166,9 @@ class GracedbRoot(APIView):
"signoff-statuses": dict(SignoffBase.OPERATOR_STATUS_CHOICES),
"instruments": dict(SignoffBase.INSTRUMENT_CHOICES),
"voevent-types" : dict(VOEvent.VOEVENT_TYPE_CHOICES),
"api-versions": api_settings.ALLOWED_VERSIONS,
"server-version": settings.PROJECT_VERSION,
# Maintained for backwards compatibility with client
"API_VERSIONS": api_settings.ALLOWED_VERSIONS,
})
......@@ -177,7 +196,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)
......@@ -189,13 +208,13 @@ class UserInfoView(RetrieveAPIView):
#def get_serializer_class(self):
# # Override so we can use custom behavior for unauthenticated users
# if self.request.user.is_anonymous():
# if self.request.user.is_anonymous:
# return AnonymousUserSerializer
# else:
# return self.serializer_class
def retrieve(self, request, *args, **kwargs):
if request.user.is_anonymous():
if request.user.is_anonymous:
output = {'username': 'AnonymousUser'}
else:
instance = request.user
......
......@@ -10,6 +10,8 @@ from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from api.utils import ResponseThenRun
# Set up logger
logger = logging.getLogger(__name__)
......@@ -88,6 +90,20 @@ class SafeCreateMixin(mixins.CreateModelMixin):
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
class ResponseThenRunMixin(mixins.CreateModelMixin):
"""
Copy of the CreateModelMixin which hijacks the response object
run a function afterward.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return ResponseThenRun(serializer.data, status=status.HTTP_201_CREATED,
headers=headers, callback=serializer.resp_callback,
callback_kwargs=serializer.resp_callback_kwargs)
class OrderedListModelMixin(object):
"""
......