From c9d564245554ba85098e638e2b01afd6ce3b2777 Mon Sep 17 00:00:00 2001
From: Brian Moe <brian.moe@ligo.org>
Date: Fri, 30 Nov 2012 14:19:52 -0600
Subject: [PATCH] Misc

---
 gracedb/api.py       | 320 ++++++++++++++++++++++++++++++++++++++-----
 gracedb/models.py    |   6 +-
 gracedb/urls_rest.py |  29 ++--
 3 files changed, 308 insertions(+), 47 deletions(-)

diff --git a/gracedb/api.py b/gracedb/api.py
index e7e3f988e..624f5687c 100644
--- a/gracedb/api.py
+++ b/gracedb/api.py
@@ -8,7 +8,8 @@ from django.conf import settings
 
 import json
 
-from gracedb.models import Event, Group
+from gracedb.models import Event, Group, EventLog, Label
+from translator import handle_uploaded_data
 
 import os
 import urllib
@@ -24,6 +25,8 @@ PAGINATE_BY = REST_FRAMEWORK_SETTINGS.get('PAGINATE_BY', 10)
 # rest_framework
 from rest_framework import serializers, status
 from rest_framework.response import Response
+from rest_framework.parsers import BaseParser
+#from rest_framework import generics
 #from rest_framework.renderers import JSONRenderer, JSONPRenderer
 #from rest_framework.renderers import YAMLRenderer, XMLRenderer
 from forms import CreateEventForm
@@ -51,15 +54,28 @@ class LigoAuthentication(authentication.BaseAuthentication):
         return (user, None)
 
 
-class EventSerializer(serializers.Serializer):
-    group        = serializers.CharField(required=True, max_length=100)
-    analysisType = serializers.CharField(required=True, max_length=100)
+class EventSerializer(serializers.ModelSerializer):
+    #group        = serializers.CharField(required=True, max_length=100)
+    #analysisType = serializers.CharField(required=True, max_length=100)
+    group = serializers.CharField(source="group.name")
+    class Meta:
+        model = Event
+        fields = ('far', 'instruments', 'group')
 
+class EventLogSerializer(serializers.ModelSerializer):
+    """docstring for EventLogSerializer"""
+    comment =  serializers.CharField(required=True, max_length=200)
+    class Meta:
+        model = EventLog
+        fields = ('comment', 'issuer', 'created')
+
+#==================================================================
+# Events
 
 def eventToDict(event, columns=None, request=None):
     """Convert an Event to a dictionary so it can be serialized.  (ugh)"""
 
-    # XXX Seems wrong.  Need to understand serializers.
+    # XXX  Need to understand serializers.
 
     rv = {}
 
@@ -69,20 +85,27 @@ def eventToDict(event, columns=None, request=None):
     rv['group'] = event.group.name
     rv['graceid'] = graceid
     rv['analysisType'] = event.get_analysisType_display()
+    rv['gpstime'] = event.gpstime
     rv['instruments'] = event.instruments
     rv['nevents'] = event.nevents
     rv['far'] = event.far
     rv['likelihood'] = event.likelihood
-    rv['labels'] = [labelling.label.name
-            for labelling in event.labelling_set.all()]
+    rv['labels'] = dict([
+            (labelling.label.name,
+                reverse("labels",
+                    args=[graceid, labelling.label.name],
+                    request=request))
+            for labelling in event.labelling_set.all()])
     rv['links'] = {
-            "neighbors" : dict(
-                [(e.gpstime, reverse("event-detail", args=[e.graceid()]))
-                    for e in event.neighbors()]),
+#           "neighbors" : dict(
+#               [(e.gpstime, reverse("event-detail", args=[e.graceid()], request=request))
+#                   for e in event.neighbors()]),
+            "neighbors" : reverse("neighbors", args=[graceid], request=request),
             "data"  : event.weburl(),
             "log"   : reverse("eventlog-list", args=[graceid], request=request),
             "files" : reverse("files", args=[graceid], request=request),
             "filemeta" : reverse("filemeta", args=[graceid], request=request),
+            "labels" : reverse("labels", args=[graceid], request=request),
             "self"  : reverse("event-detail", args=[graceid], request=request),
             }
     return rv
@@ -165,21 +188,28 @@ class EventList(APIView):
         start = int(start)
         count = int(count)
         numRows = events.count()
-        last = max(0, (count / numRows) - 1)
+        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 = reverse('event-list', request=request)
+
+        links['self'] = request.build_absolute_uri()
+
         d = { 'start' : 0, "count": count, "sort": sort }
         if query: d['query'] = query
-        rv['first'] = baseuri + "?" + urllib.urlencode(d)
+        links['first'] = baseuri + "?" + urllib.urlencode(d)
+
         d['start'] = last
-        rv['last'] = baseuri + "?" + urllib.urlencode(d)
-        rv['self'] = request.build_absolute_uri()
+        links['last'] = baseuri + "?" + urllib.urlencode(d)
+
         if start != last:
-            d['start'] = start+1
-            rv['next'] = baseuri + "?" + urllib.urlencode(d)
+            d['start'] = start+count
+            links['next'] = baseuri + "?" + urllib.urlencode(d)
         rv['numRows'] = events.count()
+        d['links'] = links
         return Response(rv)
 
     def post(self, request, format=None):
@@ -204,9 +234,42 @@ class EventList(APIView):
                     for key in form.errors]
             return Response(rv, status=status.HTTP_400_BAD_REQUEST)
 
+
+class LigoLwParser(parsers.MultiPartParser):
+    # XXX Revisit this.
+    #  Doing it right involves refactoring translator.py
+    media_type = "multipart/form-data"
+    def parse(self, *args, **kwargs):
+        data = parsers.MultiPartParser.parse(self, *args, **kwargs)
+        return data
+
+#       eventData = data.files['eventFile'].read()
+
+#       dirPrefix = settings.GRACEDB_DATA_DIR
+#       eventDir = os.path.join(dirPrefix, event.graceid())
+#       # XXX handle duplicate file names.
+#       f = request.FILES['eventFile']
+#       uploadDestination = os.path.join(eventDir, "private", f.name)
+#       fdest = open(uploadDestination, 'w')
+#       # Save uploaded file into user private area.
+#       for chunk in f.chunks():
+#           fdest.write(chunk)
+#       fdest.close()
+
+#       # Extract Info from uploaded data
+#       try:
+#           handle_uploaded_data(event, uploadDestination)
+#       except:
+#           return Response("Bad Data",
+#                   status=status.HTTP_400_BAD_REQUEST)
+#       return Response(status=status.HTTP_202_ACCEPTED)
+
+
 class EventDetail(APIView):
     authentication_classes = (LigoAuthentication,)
-    parser_classes = (parsers.MultiPartParser,)
+    parser_classes = (LigoLwParser,)
+    #parser_classes = (parsers.MultiPartParser,)
+    serializer_class = EventSerializer
     form = CreateEventForm
 
     def get(self, request, graceid):
@@ -214,21 +277,151 @@ class EventDetail(APIView):
             event = Event.getByGraceid(graceid)
         except Event.DoesNotExist:
             # XXX Real error message.
-            return Response("blah blah blah", status=status.HTTP_404_NOT_FOUND)
-        return Response(eventToDict(event, request=request))
+            return Response("Event Not Found",
+                    status=status.HTTP_404_NOT_FOUND)
+
+        #response = Response(self.serializer_class(event).data)
+        response = Response(eventToDict(event, request=request))
+
+        response["Cache-Control"] = "no-cache"
+        return response
 
     def put(self, request, graceid):
         """ I am a doc.  Do I not get put anywhere? """
-        raise NotImplementedError()
+        try:
+            event = Event.getByGraceid(graceid)
+        except Event.DoesNotExist:
+            return Response("Event Not Found",
+                    status=status.HTTP_404_NOT_FOUND)
+
+        try:
+            if request.ligouser != event.submitter:
+                msg = "You (%s) Them (%s)" % (request.ligouser, event.submitter)
+                return HttpResponseForbidden("You did not create this event. %s" %msg)
+        except Exception, e:
+            return Response(str(e))
+
+#       messages = []
+#       if event.group.name != request.DATA['group']:
+#           messages += [
+#                   "Existing event group ({0}) does not match "
+#                   "replacement event group ({1})".format(
+#                       event.group.name, request.DATA['group'])]
+#       if event.analysisType != request.DATA['type']:
+#           messages += [
+#                   "Existing event type ({0}) does not match "
+#                   "replacement event type ({1})".format(
+#                       event.analysisType, request.DATA['type'])]
+#       if messages:
+#           return Response("\n".join(messages),
+#                   status=status.HTTP_400_BAD_REQUEST)
+
+        dirPrefix = settings.GRACEDB_DATA_DIR
+        eventDir = os.path.join(dirPrefix, event.graceid())
+        # XXX handle duplicate file names.
+        f = request.FILES['eventFile']
+        uploadDestination = os.path.join(eventDir, "private", f.name)
+        fdest = open(uploadDestination, 'w')
+        # Save uploaded file into user private area.
+        for chunk in f.chunks():
+            fdest.write(chunk)
+        fdest.close()
+
+        # Extract Info from uploaded data
+        try:
+            handle_uploaded_data(event, uploadDestination)
+            event.submitter = request.ligouser
+        except:
+            return Response("Bad Data",
+                    status=status.HTTP_400_BAD_REQUEST)
+        return Response(status=status.HTTP_202_ACCEPTED)
 
+#==================================================================
+# Neighbors
+
+class EventNeighbors(APIView):
+    def get(self, request, graceid):
+        try:
+            event = Event.getByGraceid(graceid)
+        except Event.DoesNotExist:
+            # XXX Real error message.
+            return Response("Event does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        if request.QUERY_PARAMS.has_key('delta'):
+            delta = request.QUERY_PARAMS['delta']
+            if delta.find(',') < 0:
+                delta = delta2 = int(delta)
+            else:
+                delta , delta2 = map(int, delta.split(','))
+            neighbors = event.neighbors(delta=delta, delta2=delta2)
+        else:
+            neighbors = event.neighbors()
+        return Response(
+                [eventToDict(neighbor, request=request)
+                    for neighbor in neighbors])
+
+#==================================================================
+# Labels
+
+# XXX NOT FINISHED
+
+def labelToDict(label, request=None):
+    return { 
+            "name" : label.label.name,
+            "creator" : label.creator.name,
+            "created" : label.created,
+            "self" : reverse("labels",
+                args=[label.event.graceid(), label.label.name],
+                request=request),
+           }
+
+class EventLabel(APIView):
+    """Event Label"""
+    authentication_classes = (LigoAuthentication,)
+
+    def get(self, request, graceid, label):
+        event = Event.getByGraceid(graceid)
+        if label is not None:
+            theLabel = event.labelling_set.filter(label__name=label).all()
+            if len(theLabel) < 1:
+                return Response("Label Not %s Found" % label,
+                        status=status.HTTP_404_NOT_FOUND)
+            theLabel = theLabel[0]
+            return Response(labelToDict(theLabel, request=request))
+        else:
+            labels = [map(
+                lambda x: labelToDict(x,request=request),
+                event.labelling_set.all())]
+            return Response({
+                'links' : [{
+                    'self': request.build_absolute_uri(),
+                    'event': reverse("event-detail",
+                        args=[event.graceid()],
+                        request=request),
+                    }],
+                'labels': labels
+                })
+
+    def put(self, request, graceid, label):
+        return Response("Not Implemented", status=status.HTTP_501_NOT_IMPLEMENTED)
+
+    def delete(self, request, graceid, label):
+        return Response("Not Implemented", status=status.HTTP_501_NOT_IMPLEMENTED)
+
+#==================================================================
+# EventLog
+
+# Janky serialization
 def eventLogToDict(log, n=None, request=None):
     # XXX Messy.  n should not be here but in the model.
-    if n is None and request:
+    if (n is None) and request:
         uri = request.build_absolute_uri()
     elif n is not None and request:
-        uri = reverse("eventlog-detail", args=[log.event.graceid(), n], request=request)
+        uri = reverse("eventlog-detail",
+                args=[log.event.graceid(), n],
+                request=request)
     else:
-        uri = ""
+        uri = None
     return {
                 "comment" : log.comment,
                 "created" : log.created,
@@ -237,6 +430,10 @@ def eventLogToDict(log, n=None, request=None):
            }
 
 class EventLogList(APIView):
+    """Event Log List Resource
+
+    POST param 'message'
+    """
     authentication_classes = (LigoAuthentication,)
 
     def get(self, request, graceid):
@@ -244,16 +441,30 @@ class EventLogList(APIView):
             event = Event.getByGraceid(graceid)
         except Event.DoesNotExist:
             # XXX Real error message.
-            return Response("blah blah blah", status=status.HTTP_404_NOT_FOUND)
-        logset = event.eventlog_set
+            return Response("Event does not exist.",
+                    status=status.HTTP_404_NOT_FOUND)
+        logset = event.eventlog_set.order_by("created")
         count = logset.count()
         rv = [ eventLogToDict(log, n, request)
                 for (n, log) in zip(range(0,count+2), logset.iterator()) ]
         return Response(rv)
 
-class EventLogDetail(APIView):
-    """docstring for EventLogDetail"""
+    def post(self, request, graceid):
+        event = Event.getByGraceid(graceid)
+        message = request.DATA.get('message')
+        logentry = EventLog(
+                event=event,
+                issuer=request.ligouser,
+                comment=message)
+        logset = event.eventlog_set.order_by("created")
+        n = len(logset)
+        logentry.save()
+        rv = eventLogToDict(logentry, n, request=request)
+        response = Response(rv, status=status.HTTP_201_CREATED)
+        response['Location'] = rv['self']
+        return response
 
+class EventLogDetail(APIView):
     authentication_classes = (LigoAuthentication,)
 
     def get(self, request, graceid, n):
@@ -261,10 +472,14 @@ class EventLogDetail(APIView):
             event = Event.getByGraceid(graceid)
         except Event.DoesNotExist:
             # XXX Real error message.
-            return Response("blah blah blah", status=status.HTTP_404_NOT_FOUND)
-        rv = event.eventlog_set.all()[int(n)]
+            return Response("Log Entry Not Found",
+                    status=status.HTTP_404_NOT_FOUND)
+        rv = event.eventlog_set.order_by("created").all()[int(n)]
         return Response(eventLogToDict(rv, request=request))
 
+#==================================================================
+# Root Resource
+
 class GracedbRoot(APIView):
     """
         Root of the Gracedb REST API
@@ -286,14 +501,24 @@ class GracedbRoot(APIView):
         filemeta = filemeta.replace("G1200", "{graceid}")
         filemeta = filemeta.replace("filename", "{filename}")
 
+        labels = reverse('labels', args=["G1200", "thelabel"], request=request)
+        labels = labels.replace("G1200", "{graceid}")
+        labels = labels.replace("thelabel", "{label}")
+
         return Response({
-                "event-list" : reverse("event-list", request=request),
-                "event-detail-template" : detail,
+            "resources" : {
+                "events" : reverse("event-list", request=request),
+            },
+            "resource-templates" : {
+                "event-template" : detail,
                 "event-log-template" : log,
-                "files-template" : files,
-                "filemeta-template" : filemeta,
+                "event-files-template" : files,
+                "event-filemeta-template" : filemeta,
+                "event-label-template" : labels,
+            },
                 "groups" : [group.name for group in Group.objects.all()],
                 "analysis-types" : dict(Event.ANALYSIS_TYPE_CHOICES),
+                "labels" : [label.name for label in Label.objects.all()],
                })
 
 ##################################################################
@@ -363,8 +588,8 @@ class Files(APIView):
     authentication_classes = (LigoAuthentication,)
     parser_classes = (parsers.MultiPartParser,)
 
-    def get(self, request, graceid, filename=""):
-        # Do not filename to be None.  That messes up later os.path.join
+    def get(self, request, graceid, filename=None):
+        # Do not let filename be None.  That messes up later os.path.join
         filename = filename or ""
 
         try:
@@ -372,7 +597,7 @@ class Files(APIView):
         except Event.DoesNotExist:
             return HttpResponseNotFound("Event not found")
 
-        # The plan to deal with that wretched general/ directory maybe
+        # The plan to deal with that general/ directory maybe
         # should be to move it INTO private.  Then externally, things
         # would look like they do now, but the code here would be MUCH
         # more sane and much shorter.
@@ -397,12 +622,19 @@ class Files(APIView):
             # Get list of files w/urls.
             rv = {}
             filepath = event.datadir()
+            files = []
             for dirname, dirnames, filenames in os.walk(filepath):
                 dirname = dirname[len(filepath):]  # cut off base event dir path
                 for filename in filenames:
                     # relative path from root of event data dir
                     filename = os.path.join(dirname, filename)
                     rv[filename] = reverse("files", args=[graceid, filename], request=request)
+                    files.append({
+                            'name' : filename,
+                            'link' :  reverse("files",
+                                args=[graceid, filename],
+                                request=request),
+                            })
 
             # XXX UGH...  that awful general/ dir
             # Actually not terrible, but do not like private/general as siblings.
@@ -415,10 +647,26 @@ class Files(APIView):
                     # relative path from root of event data dir
                     filename = os.path.join(dirname, filename)
                     rv[filename] = reverse("files", args=[graceid, filename], request=request)
+                    files.append({
+                            'name' : filename,
+                            'link' :  reverse("files",
+                                args=[graceid, filename],
+                                request=request),
+                            })
 
             #response = HttpResponse(simplejson.dumps(rv), content_type="application/json")
+#           response = Response({
+#               'links' : {
+#                   'self' : request.build_absolute_uri(),
+#                   'event' : reverse("event-detail",
+#                       args=[graceid],
+#                       request=request),
+#                   },
+#               'files' : files,
+#               })
             response = Response(rv)
         elif os.path.isdir(filepath):
+            # XXX Really?
             response = HttpResponseForbidden("%s is a directory" % filename)
         else:
             response = HttpResponseServerError("Should not happen.")
diff --git a/gracedb/models.py b/gracedb/models.py
index bf1f1eb7d..ea901390e 100644
--- a/gracedb/models.py
+++ b/gracedb/models.py
@@ -123,14 +123,16 @@ class Event(models.Model):
             gps_time = int(posixToGpsTime(posix_time))
             return gps_time - self.gpstime
 
-    def neighbors(self, delta=5):
+    def neighbors(self, delta=5, delta2=None):
         if not self.gpstime:
             return []
         if self.group.name == 'Test':
             nearby = Event.objects.filter(group__name='Test')
         else:
             nearby = Event.objects.exclude(group__name='Test')
-        nearby = nearby.filter(gpstime__range=(self.gpstime-delta, self.gpstime+delta))
+        if delta2 is None:
+            delta2 = delta
+        nearby = nearby.filter(gpstime__range=(self.gpstime-delta, self.gpstime+delta2))
         nearby = nearby.exclude(id=self.id)
         nearby = nearby.order_by('gpstime')
         return nearby
diff --git a/gracedb/urls_rest.py b/gracedb/urls_rest.py
index 9a9ae8d61..86f9794af 100644
--- a/gracedb/urls_rest.py
+++ b/gracedb/urls_rest.py
@@ -6,36 +6,47 @@ from gracedb.api import GracedbRoot
 from gracedb.api import EventList, EventDetail
 from gracedb.api import EventLogList, EventLogDetail
 from gracedb.api import Files, FileMeta
+from gracedb.api import EventNeighbors, EventLabel
 
 urlpatterns = patterns('gracedb.api',
     url (r'^$', GracedbRoot.as_view(), name="api-root"),
 
     # Event Resources
     # events/[{graceid}[/{version}]]
-    url (r'events/$', EventList.as_view(), name='event-list'),
-    url (r'events/(?P<graceid>[GEHT]\d+)$', EventDetail.as_view(), name='event-detail'),
+    url (r'events/$',
+        EventList.as_view(), name='event-list'),
+    url (r'events/(?P<graceid>[GEHT]\d+)$',
+        EventDetail.as_view(), name='event-detail'),
+
     # Event Log Resources
     # events/{graceid}/logs/[{logid}]
-    url (r'events/(?P<graceid>[GEHT]\d+)/log/$', EventLogList.as_view(), name='eventlog-list'),
-    url (r'events/(?P<graceid>[GEHT]\d+)/log/(?P<n>\d+)$', EventLogDetail.as_view(), name='eventlog-detail'),
+    url (r'events/(?P<graceid>[GEHT]\d+)/log/$',
+        EventLogList.as_view(), name='eventlog-list'),
+    url (r'events/(?P<graceid>[GEHT]\d+)/log/(?P<n>\d+)$',
+        EventLogDetail.as_view(), name='eventlog-detail'),
 
     # Event File Resources
     # events/{graceid}/files/[{filename}[/{version}]]
-    # XXX change to DjangoRF.  But this works now with the cli.
-    #url (r'^events/(?P<graceid>[\w\d]+)/files/(?P<filename>.+)?$', 'download', name="files"),
-    url (r'^events/(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$', Files.as_view(), name="files"),
+    url (r'^events/(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$',
+        Files.as_view(), name="files"),
     # events/{graceid}/filemeta/[{filename}]
-    url (r'^events/(?P<graceid>\w[\d]+)/filemeta/(?P<filename>.+)?$', FileMeta.as_view(), name="filemeta"),
+    url (r'^events/(?P<graceid>\w[\d]+)/filemeta/(?P<filename>.+)?$',
+        FileMeta.as_view(), name="filemeta"),
 
     # Event Labels
     # events/{graceid}/labels/[{label}]
+    url (r'^events/(?P<graceid>\w[\d]+)/labels/(?P<label>.+)?$',
+        EventLabel.as_view(), name="labels"),
 
     # Event Slots
     # events/{graceid}/slots/[{slotid}]
 
     # Event Neighbors
-    # events/{graceid}/neighbors/[?neighborhood=N]
+    # events/{graceid}/neighbors/[?delta=(N|(N,N))]
+    url (r'^events/(?P<graceid>\w[\d]+)/neighbors/$',
+        EventNeighbors.as_view(), name="neighbors"),
 
     # Legacy
+    #url (r'^events/(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$', 'download', name="files"),
     url (r'^event/(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$', 'download', name="download2"),
 )
-- 
GitLab