diff --git a/gracedb/api.py b/gracedb/api.py index 11157c5a8a586e204064ad1f0502f6fce41b3935..6e4cd78e93325d3d2bf2712398eebd3464ef1dfc 100644 --- a/gracedb/api.py +++ b/gracedb/api.py @@ -8,7 +8,7 @@ from django.conf import settings import json -from gracedb.models import Event, Group, EventLog, Slot +from gracedb.models import Event, Group, EventLog, Slot, Tag from gracedb.views import create_label from translator import handle_uploaded_data @@ -115,6 +115,7 @@ def eventToDict(event, columns=None, request=None): "filemeta" : reverse("filemeta", 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), } # XXX Jam the slots in here? Could just have a list of slot names instead of # all these links. But the links might be useful?? @@ -472,12 +473,16 @@ class EventLabel(APIView): # Janky serialization def eventLogToDict(log, n=None, request=None): # XXX Messy. n should not be here but in the model. + taglist_uri = None 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) + taglist_uri = reverse("eventlogtag-list", + args=[log.event.graceid(), n], + request=request) else: uri = None return { @@ -485,6 +490,7 @@ def eventLogToDict(log, n=None, request=None): "created" : log.created, "issuer" : log.issuer.name, "self" : uri, + "tags" : taglist_uri, } class EventLogList(APIView): @@ -547,7 +553,254 @@ class EventLogDetail(APIView): 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)) + # XXX I (Branson) put the n argument here. Why not? + # We might as well since we have it, right? + return Response(eventLogToDict(rv, n, request=request)) + +#================================================================== +# Tags + + +def tagToDict(tag, columns=None, request=None, event=None, n=None): + """Convert a tag to a dictionary. + Output depends on the level of specificity. + """ + + rv = {} + rv['name'] = tag.name + rv['displayName'] = tag.displayName + if event: + if n: + # We want a link to the self only. End of the line. + rv['links'] = { + "self" : reverse("eventlogtag-detail", + args=[event.graceid(),n,tag.name], + request=request) + } + else: + # Links to all log messages of the event with this tag. + rv['links'] = { + "logs" : [reverse("eventlog-detail", + args=[event.graceid(),log.getN()], + request=request) + for log in event.getLogsForTag(tag.name)], + "self" : reverse("eventtag-detail", + args=[event.graceid(),tag.name], + request=request) + } + else: + # Links to all events that have this tag. + rv['links'] = { + "events" : [reverse("event-detail", + args=[event.graceid()], + request=request) + for event in tag.getEvents()], + "self" : reverse("tag-detail", + args=[tag.name], + request=request) + } + return rv + +class TagList(APIView): + """Tag List Resource + """ + authentication_classes = (LigoAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request): + # Return a list of links to all tag objects. + rv = { + 'tags' : [ reverse("tag-detail", args=[tag.name], + request=request) + for tag in Tag.objects.all() ] + } + return Response(rv) + +class TagDetail(APIView): + """Tag Detail Resource + """ + authentication_classes = (LigoAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request, tagname): + try: + tag = Tag.objects.filter(name=tagname)[0] + except Tag.DoesNotExist: + return Response("Tag not found.", + status=status.HTTP_404_NOT_FOUND) + return Response(tagToDict(tag,request=request)) + +class EventTagList(APIView): + """Event Tag List Resource + """ + authentication_classes = (LigoAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request, graceid): + # Return a list of links to all tags for this event. + try: + event = Event.getByGraceid(graceid) + except Event.DoesNotExist: + # XXX Real error message. + return Response("Event does not exist.", + status=status.HTTP_404_NOT_FOUND) + + rv = { + 'tags' : [ reverse("eventtag-detail",args=[graceid, + tag.name], + request=request) + for tag in event.getAvailableTags()] + } + + return Response(rv) + +class EventTagDetail(APIView): + """Event Tag List Resource + """ + authentication_classes = (LigoAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request, graceid, tagname): + try: + event = Event.getByGraceid(graceid) + except Event.DoesNotExist: + # XXX Real error message. + return Response("Event does not exist.", + status=status.HTTP_404_NOT_FOUND) + try: + tag = Tag.objects.filter(name=tagname)[0] + rv = tagToDict(tag,event=event,request=request) + return Response(rv) + except Tag.DoesNotExist: + return Response("No such tag for event.", + status=status.HTTP_404_NOT_FOUND) + +class EventLogTagList(APIView): + """Event Log Tag List Resource + """ + authentication_classes = (LigoAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request, graceid, n): + # Return a list of links to tags associated with a given log message + try: + event = Event.getByGraceid(graceid) + eventlog = event.eventlog_set.order_by("created").all()[int(n)] + except Event.DoesNotExist: + # XXX Real error message. + return Response("Event does not exist.", + status=status.HTTP_404_NOT_FOUND) + except: + # XXX Real error message. + return Response("Log does not exist.", + status=status.HTTP_404_NOT_FOUND) + + rv = { + 'tags' : [ reverse("eventlogtag-detail", + args=[graceid, + n, tag.name], + request=request) + for tag in eventlog.tag_set.all()] + } + + return Response(rv) + +class EventLogTagDetail(APIView): + """Event Log Tag Detail Resource + """ + authentication_classes = (LigoAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request, graceid, n, tagname): + try: + event = Event.getByGraceid(graceid) + eventlog = event.eventlog_set.order_by("created").all()[int(n)] + except Event.DoesNotExist: + # XXX Real error message. + return Response("Event does not exist.", + status=status.HTTP_404_NOT_FOUND) + except: + # XXX Real error message. + return Response("Log does not exist.", + status=status.HTTP_404_NOT_FOUND) + try: + tag = eventlog.tag_set.filter(name=tagname)[0] + # Serialize + return Response(tagToDict(tag,event=event,n=n,request=request)) + except: + return Response("Tag not found.",status=status.HTTP_404_NOT_FOUND) + + def put(self, request, graceid, n, tagname): + logger = logging.getLogger(__name__) + try: + event = Event.getByGraceid(graceid) + eventlog = event.eventlog_set.order_by("created").all()[int(n)] + except Event.DoesNotExist: + # XXX Real error message. + return Response("Event does not exist.", + status=status.HTTP_404_NOT_FOUND) + except: + # XXX Real error message. + return Response("Log does not exist.", + status=status.HTTP_404_NOT_FOUND) + try: + # Has this tag-eventlog relationship already been created? If so, kick out. + # Actually, adding the eventlog to the tag would not hurt anything--no + # duplicate entry would be made in the database. However, we don't want + # an extra log entry, or a deceptive HTTP response (i.e., one telling the + # client that the creation was sucessful when, in fact, the database + # was unchanged. + tag = eventlog.tag_set.filter(name=tagname)[0] + msg = "Log already has tag %s" % unicode(tag) + return Response(msg,status=status.HTTP_409_CONFLICT) + except: + # Look for the tag. If it doesn't already exist, create it. + try: + tag = Tag.objects.filter(name=tagname)[0] + except: + displayName = request.DATA.get('filename') + tag = Tag(name=tagname, displayName=displayName) + tag.save() + + # Now add the log message to this tag. + tag.eventlogs.add(eventlog) + + # Create a log entry to document the tag creation. + msg = "Tagged message %s: %s " % (n, tagname) + logentry = EventLog(event=event, + issuer=request.ligouser, + comment=msg) + logentry.save() + + return Response("Tag created.",status=status.HTTP_201_CREATED) + + def delete(self, request, graceid, n, tagname): + try: + event = Event.getByGraceid(graceid) + eventlog = event.eventlog_set.order_by("created").all()[int(n)] + except Event.DoesNotExist: + # XXX Real error message. + return Response("Event does not exist.", + status=status.HTTP_404_NOT_FOUND) + except: + # XXX Real error message. + return Response("Log does not exist.", + status=status.HTTP_404_NOT_FOUND) + try: + tag = eventlog.tag_set.filter(name=tagname)[0] + tag.delete() + return Response("Tag deleted.",status=status.HTTP_200_OK) + + # Create a log entry to document the tag creation. + msg = "Removed tag %s for message %s. " % (tagname, n) + logentry = EventLog(event=event, + issuer=request.ligouser, + comment=msg) + logentry.save() + + except: + return Response("Tag not found.",status=status.HTTP_404_NOT_FOUND) + #================================================================== # Root Resource @@ -583,6 +836,17 @@ class GracedbRoot(APIView): slot = slot.replace("G1200", "{graceid}") slot = slot.replace("slotname", "{slotname}") + taglist = reverse("eventlogtag-list", args=["G1200", "0"], request=request) + taglist = taglist.replace("G1200", "{graceid}") + taglist = taglist.replace("0", "{n}") + + tag = reverse("eventlogtag-detail", args=["G1200", "0", "tagname"], request=request) + tag = tag.replace("G1200", "{graceid}") + tag = tag.replace("0", "{n}") + tag = tag.replace("tagname", "{tagname}") + + # XXX Need a template for the tag list? + templates = { "event-detail-template" : detail, "event-log-template" : log, @@ -590,6 +854,8 @@ class GracedbRoot(APIView): "files-template" : files, "filemeta-template" : filemeta, "slot-template" : slot, + "tag-template" : tag, + "taglist-template" : taglist, } return Response({ @@ -884,10 +1150,25 @@ class EventSlot(APIView): slot = Slot.objects.filter(event=event).filter(name=slotname)[0] slot.value = filename slot.save() + # Create a log entry to document the slot update. + msg = "Updated slot %s with file " % slotname + logentry = EventLog(event=event, + issuer=request.ligouser, + filename=tmpFilename, + comment=msg) + logentry.save() except: # Create the slot. slot = Slot(event=event,name=slotname,value=filename) slot.save() + # Create a log entry to document the slot creation. + msg = "Created slot %s with file " % slotname + logentry = EventLog(event=event, + issuer=request.ligouser, + filename=tmpFilename, + comment=msg) + logentry.save() + return Response("Slot created or updated.",status=status.HTTP_201_CREATED) # Delete a slot. @@ -909,5 +1190,12 @@ class EventSlot(APIView): status=status.HTTP_404_NOT_FOUND) slot.delete() + # Create a log entry to document the slot destruction. + msg = "Deleted slot %s " % slotname + logentry = EventLog(event=event, + issuer=request.ligouser, + comment=msg) + logentry.save() + return Response("Slot deleted.",status=status.HTTP_200_OK) diff --git a/gracedb/models.py b/gracedb/models.py index 0b19353a4dfea6dd0c4605ee295f3fa133c92ffc..2e33a9737f536c5dacffb40b32727cda0e6054ca 100644 --- a/gracedb/models.py +++ b/gracedb/models.py @@ -171,6 +171,25 @@ class Event(models.Model): def __unicode__(self): return self.graceid() + # Return a list of distinct tags associated with the log messages of this + # event. + def getAvailableTags(self): + tagset_list = [log.tag_set.all() for log in self.eventlog_set.all()] + taglist = [] + for tagset in tagset_list: + for tag in tagset: + taglist.append(tag) + # Eliminate duplicates + return list(set(taglist)) + + def getLogsForTag(self,tagname): + loglist = [] + for log in self.eventlog_set.all(): + for tag in log.tag_set.all(): + if tag.name==tagname: + loglist.append(log) + return loglist + class EventLog(models.Model): class Meta: ordering = ["-created"] @@ -191,6 +210,16 @@ class EventLog(models.Model): # XXX hacky return self.filename and self.filename[-3:].lower() in ['png','gif','jpg'] + def getN(self): + # XXX also hacky? + # I think it would still work if some logs were removed from the database. + logset = self.event.eventlog_set.order_by("created") + # XXX This actually evaluates the queryset. This may be a problem if + # there are a huge number of log messages for this event and they + # take up a lot of memory + logset = list(logset) + return logset.index(self) + class Labelling(models.Model): event = models.ForeignKey(Event) label = models.ForeignKey(Label) @@ -238,8 +267,32 @@ class MultiBurstEvent(Event): ligo_angle = models.FloatField(null=True) ligo_angle_sig = models.FloatField(null=True) -## Slots (user-defined event attributes) +## Tags (user-defined log message attributes) +class Tag(models.Model): + """Slot Model""" + # XXX Does the tag need to have a submitter column? + # No, because creating a tag will generate a log message. + # For the same reason, a timstamp is not necessary. + eventlogs = models.ManyToManyField(EventLog) + name = models.CharField(max_length=100) + displayName = models.CharField(max_length=200,null=True) + def __unicode__(self): + if self.displayName: + return self.displayName + else: + return self.name + + def getEvents(self): + # XXX Any way of doing this with filters? + # We would need to filter for a non-null intersection of the + # set of log messages in the event with the set of log + # messages in the tag. + eventlist = [log.event for log in self.eventlogs.all()] + return list(set(eventlist)) + + +## XXX Get rid of the slots. Probably. class Slot(models.Model): """Slot Model""" # Does the slot need to have a submitter column? diff --git a/gracedb/urls_rest.py b/gracedb/urls_rest.py index d915fe9e4996cdfb41625b04da04a7d4318aa751..804298351e7b16103bc3c5c0f7ed78caa5fc00c0 100644 --- a/gracedb/urls_rest.py +++ b/gracedb/urls_rest.py @@ -5,6 +5,9 @@ from django.conf.urls.defaults import patterns, url from gracedb.api import GracedbRoot from gracedb.api import EventList, EventDetail from gracedb.api import EventLogList, EventLogDetail +from gracedb.api import TagList, TagDetail +from gracedb.api import EventTagList, EventTagDetail +from gracedb.api import EventLogTagList, EventLogTagDetail from gracedb.api import EventSlot from gracedb.api import Files, FileMeta from gracedb.api import EventNeighbors, EventLabel @@ -27,6 +30,20 @@ urlpatterns = patterns('gracedb.api', url (r'events/(?P<graceid>[GEHT]\d+)/log/(?P<n>\d+)$', EventLogDetail.as_view(), name='eventlog-detail'), + # Tag Resources + url (r'^tag/$', + TagList.as_view(), name='tag-list'), + url (r'^tag/(?P<tagname>\w+)$', + TagDetail.as_view(), name='tag-detail'), + url (r'events/(?P<graceid>[GEHT]\d+)/tag/$', + EventTagList.as_view(), name='eventtag-list'), + url (r'events/(?P<graceid>[GEHT]\d+)/tag/(?P<tagname>\w+)$', + EventTagDetail.as_view(), name='eventtag-detail'), + url (r'events/(?P<graceid>[GEHT]\d+)/log/(?P<n>\d+)/tag/$', + EventLogTagList.as_view(), name='eventlogtag-list'), + url (r'events/(?P<graceid>[GEHT]\d+)/log/(?P<n>\d+)/tag/(?P<tagname>\w+)$', + EventLogTagDetail.as_view(), name='eventlogtag-detail'), + # Event File Resources # events/{graceid}/files/[{filename}[/{version}]] url (r'^events/(?P<graceid>\w[\d]+)/files/(?P<filename>.+)?$',