diff --git a/gracedb/admin.py b/gracedb/admin.py index 0920ccd302864b5c7330b54fd82b88842e6ce6f6..7e36d454e8d2ad5b3a2c1897a2a1bd103bf5505a 100644 --- a/gracedb/admin.py +++ b/gracedb/admin.py @@ -1,6 +1,6 @@ from models import Event, EventLog, User, Group -from models import Label, Labelling, Slot +from models import Label, Labelling, Tag from django.contrib import admin class EventAdmin(admin.ModelAdmin): @@ -30,8 +30,9 @@ class LabellingAdmin(admin.ModelAdmin): list_display = [ 'event', 'label', 'creator' ] search_fields = [ 'event__id', 'label__name', 'creator__name' ] -class SlotAdmin(admin.ModelAdmin): - list_display = [ 'event', 'name', 'value' ] +class TagAdmin(admin.ModelAdmin): + list_display = [ 'name', 'displayName' ] + filter_vertical = [ 'eventlogs' ] admin.site.register(Event, EventAdmin) admin.site.register(EventLog, EventLogAdmin) @@ -39,4 +40,4 @@ admin.site.register(User, UserAdmin) admin.site.register(Group) admin.site.register(Label, LabelAdmin) admin.site.register(Labelling, LabellingAdmin) -admin.site.register(Slot, SlotAdmin) +admin.site.register(Tag, TagAdmin) diff --git a/gracedb/api.py b/gracedb/api.py index 11157c5a8a586e204064ad1f0502f6fce41b3935..9b4fe0c753aa15eea22351badb600b8ca49f9608 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, Tag from gracedb.views import create_label from translator import handle_uploaded_data @@ -115,12 +115,8 @@ 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?? - rv['slots'] = {} - for slot in Slot.objects.filter(event=event).order_by('name'): - rv['slots'][slot.name] = reverse("slot", args=[graceid, slot.name], request=request) return rv @@ -472,12 +468,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 +485,7 @@ def eventLogToDict(log, n=None, request=None): "created" : log.created, "issuer" : log.issuer.name, "self" : uri, + "tags" : taglist_uri, } class EventLogList(APIView): @@ -521,8 +522,10 @@ class EventLogList(APIView): return Response(rv) def post(self, request, graceid): + logger = logging.getLogger(__name__) event = Event.getByGraceid(graceid) message = request.DATA.get('message') + tagname = request.DATA.get('tagname') logentry = EventLog( event=event, issuer=request.ligouser, @@ -533,6 +536,16 @@ class EventLogList(APIView): rv = eventLogToDict(logentry, n, request=request) response = Response(rv, status=status.HTTP_201_CREATED) response['Location'] = rv['self'] + + if tagname: + n = logentry.getN() + # XXX This is not what these API views are really meant for, but... + tmp = EventLogTagDetail() + retval = tmp.put(request, graceid, n, tagname) + # XXX This seems like a bizarre way of getting an error message out. + if retval.status_code != 201: + response['tagWarning'] = 'Error creating tag.' + return response class EventLogDetail(APIView): @@ -547,7 +560,264 @@ 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: + # XXX Unclear what the tag detail resource should be at this level. + # For now, return an empty list. + pass +# 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() ] +# For now, we just output the tag names, since we don't know what +# tag-detail should look like. + 'tags' : [ tag.name for tag in Tag.objects.all() ] + } + return Response(rv) + +# XXX Unclear what the tag detail resource should be. +# 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('displayName') + 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.eventlogs.remove(eventlog) + + # Is the tag empty now? If so we can delete it. + if not tag.eventlogs.all(): + tag.delete() + + # 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() + + return Response("Tag deleted.",status=status.HTTP_200_OK) + except: + return Response("Tag not found.",status=status.HTTP_404_NOT_FOUND) + #================================================================== # Root Resource @@ -579,9 +849,16 @@ class GracedbRoot(APIView): labels = labels.replace("G1200", "{graceid}") labels = labels.replace("thelabel", "{label}") - slot = reverse("slot", args=["G1200", "slotname"], request=request) - 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, @@ -589,7 +866,8 @@ class GracedbRoot(APIView): "event-label-template" : labels, "files-template" : files, "filemeta-template" : filemeta, - "slot-template" : slot, + "tag-template" : tag, + "taglist-template" : taglist, } return Response({ @@ -816,98 +1094,3 @@ class FileMeta(APIView): permission_classes = (IsAuthenticated,) pass -#================================================================== -# Slots - -class EventSlot(APIView): - """A slot associated with an event. - """ - - # Get the value of a slot. This will be a filename. - def get(self, request, graceid, slotname): - 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 not slotname: - return Response(dict([(s.name, s.value) - for s in Slot.objects.filter(event=event)])) - - try: - slot = Slot.objects.filter(event=event).filter(name=slotname)[0] - except: - # Okay, no slot yet. Probably want an error message. - # Try looking for files that contain the slot name. - return Response("No slot. Search based on slotname not implemented yet.", - status=status.HTTP_404_NOT_FOUND) - filename = slot.value - rv = {} - rv['value'] = filename - return Response(rv) - - # Create a slot. The slot's value will be a filename. - # This file is assumed to have already been uploaded. - def put(self, request, graceid, slotname): - try: - event = Event.getByGraceid(graceid) - except Event.DoesNotExist: - # XXX Real error message. - return Response("Event does not exist.", - status=status.HTTP_404_NOT_FOUND) - filename = request.DATA.get('filename') - # Interestingly, the None object seems to be converted to a string - # when encoded in the HTTP request body. Hence the 'None' string - # below. If somebody intentionally named a file 'None', then - # they deserve to get this error message. - if filename=='' or filename=='None' or filename==None: - return Response("Please submit a filename or upload a file.", - status=status.HTTP_400_BAD_REQUEST) - - # UGLY hack to deal with /private vs /general dirs - general = False - tmpFilename = filename - if filename.startswith("general/"): - tmpFilename = filename[len("general/"):] - general = True - filepath = os.path.join(event.datadir(general), tmpFilename) - - # Check for existence of the file. - if not os.path.exists(filepath): - return Response("No slot created because file does not exist", - status=status.HTTP_404_NOT_FOUND) - # Check for existence of the slot. If it exists, simply update the - # existing slot. - try: - slot = Slot.objects.filter(event=event).filter(name=slotname)[0] - slot.value = filename - slot.save() - except: - # Create the slot. - slot = Slot(event=event,name=slotname,value=filename) - slot.save() - return Response("Slot created or updated.",status=status.HTTP_201_CREATED) - - # Delete a slot. - def delete(self, request, graceid, slotname): - try: - event = Event.getByGraceid(graceid) - except Event.DoesNotExist: - # XXX Real error message. - return Response("Event does not exist.", - status=status.HTTP_404_NOT_FOUND) - - # Gotta find the poor devil before we can delete him. - try: - slot = Slot.objects.filter(event=event).filter(name=slotname)[0] - except: - # Okay, no slot yet. Probably want an error message. - # Try looking for files that contain the slot name. - return Response("No such slot.", - status=status.HTTP_404_NOT_FOUND) - - slot.delete() - return Response("Slot deleted.",status=status.HTTP_200_OK) - diff --git a/gracedb/migrations/0002_auto__add_tag.py b/gracedb/migrations/0002_auto__add_tag.py new file mode 100644 index 0000000000000000000000000000000000000000..69ffa13d4bbe5208df06f64c9557463b7edc14a7 --- /dev/null +++ b/gracedb/migrations/0002_auto__add_tag.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Tag' + db.create_table('gracedb_tag', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('displayName', self.gf('django.db.models.fields.CharField')(max_length=200, null=True)), + )) + db.send_create_signal('gracedb', ['Tag']) + + # Adding M2M table for field eventlogs on 'Tag' + db.create_table('gracedb_tag_eventlogs', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('tag', models.ForeignKey(orm['gracedb.tag'], null=False)), + ('eventlog', models.ForeignKey(orm['gracedb.eventlog'], null=False)) + )) + db.create_unique('gracedb_tag_eventlogs', ['tag_id', 'eventlog_id']) + + + def backwards(self, orm): + # Deleting model 'Tag' + db.delete_table('gracedb_tag') + + # Removing M2M table for field eventlogs on 'Tag' + db.delete_table('gracedb_tag_eventlogs') + + + models = { + 'gracedb.approval': { + 'Meta': {'object_name': 'Approval'}, + 'approvedEvent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Event']"}), + 'approver': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.User']"}), + 'approvingCollaboration': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'gracedb.coincinspiralevent': { + 'Meta': {'ordering': "['-id']", 'object_name': 'CoincInspiralEvent', '_ormbases': ['gracedb.Event']}, + 'combined_far': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'end_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'end_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'event_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['gracedb.Event']", 'unique': 'True', 'primary_key': 'True'}), + 'false_alarm_rate': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ifos': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}), + 'mass': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'mchirp': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'minimum_duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'snr': ('django.db.models.fields.FloatField', [], {'null': 'True'}) + }, + 'gracedb.event': { + 'Meta': {'ordering': "['-id']", 'object_name': 'Event'}, + 'analysisType': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'far': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'gpstime': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instruments': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}), + 'labels': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['gracedb.Label']", 'through': "orm['gracedb.Labelling']", 'symmetrical': 'False'}), + 'likelihood': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'nevents': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.User']"}), + 'uid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}) + }, + 'gracedb.eventlog': { + 'Meta': {'ordering': "['-created']", 'object_name': 'EventLog'}, + 'comment': ('django.db.models.fields.TextField', [], {}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Event']"}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.User']"}) + }, + 'gracedb.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['gracedb.User']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + 'gracedb.label': { + 'Meta': {'object_name': 'Label'}, + 'defaultColor': ('django.db.models.fields.CharField', [], {'default': "'black'", 'max_length': '20'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}) + }, + 'gracedb.labelling': { + 'Meta': {'object_name': 'Labelling'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.User']"}), + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Event']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'label': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Label']"}) + }, + 'gracedb.multiburstevent': { + 'Meta': {'ordering': "['-id']", 'object_name': 'MultiBurstEvent', '_ormbases': ['gracedb.Event']}, + 'amplitude': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'bandwidth': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'central_freq': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'confidence': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'event_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['gracedb.Event']", 'unique': 'True', 'primary_key': 'True'}), + 'false_alarm_rate': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ifos': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}), + 'ligo_angle': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ligo_angle_sig': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ligo_axis_dec': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ligo_axis_ra': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'peak_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'peak_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'snr': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'start_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'start_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}) + }, + 'gracedb.slot': { + 'Meta': {'unique_together': "(('event', 'name'),)", 'object_name': 'Slot'}, + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Event']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'gracedb.tag': { + 'Meta': {'object_name': 'Tag'}, + 'displayName': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}), + 'eventlogs': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['gracedb.EventLog']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'gracedb.user': { + 'Meta': {'ordering': "['name']", 'object_name': 'User'}, + 'dn': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'principal': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'unixid': ('django.db.models.fields.CharField', [], {'max_length': '25'}) + } + } + + complete_apps = ['gracedb'] \ No newline at end of file diff --git a/gracedb/migrations/0003_auto__del_slot__del_unique_slot_event_name.py b/gracedb/migrations/0003_auto__del_slot__del_unique_slot_event_name.py new file mode 100644 index 0000000000000000000000000000000000000000..066c22bff6271796213757f1de8e4df130409556 --- /dev/null +++ b/gracedb/migrations/0003_auto__del_slot__del_unique_slot_event_name.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Removing unique constraint on 'Slot', fields ['event', 'name'] + db.delete_unique('gracedb_slot', ['event_id', 'name']) + + # Deleting model 'Slot' + db.delete_table('gracedb_slot') + + + def backwards(self, orm): + # Adding model 'Slot' + db.create_table('gracedb_slot', ( + ('value', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('event', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['gracedb.Event'])), + ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + )) + db.send_create_signal('gracedb', ['Slot']) + + # Adding unique constraint on 'Slot', fields ['event', 'name'] + db.create_unique('gracedb_slot', ['event_id', 'name']) + + + models = { + 'gracedb.approval': { + 'Meta': {'object_name': 'Approval'}, + 'approvedEvent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Event']"}), + 'approver': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.User']"}), + 'approvingCollaboration': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'gracedb.coincinspiralevent': { + 'Meta': {'ordering': "['-id']", 'object_name': 'CoincInspiralEvent', '_ormbases': ['gracedb.Event']}, + 'combined_far': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'end_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'end_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'event_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['gracedb.Event']", 'unique': 'True', 'primary_key': 'True'}), + 'false_alarm_rate': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ifos': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}), + 'mass': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'mchirp': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'minimum_duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'snr': ('django.db.models.fields.FloatField', [], {'null': 'True'}) + }, + 'gracedb.event': { + 'Meta': {'ordering': "['-id']", 'object_name': 'Event'}, + 'analysisType': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'far': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'gpstime': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instruments': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}), + 'labels': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['gracedb.Label']", 'through': "orm['gracedb.Labelling']", 'symmetrical': 'False'}), + 'likelihood': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'nevents': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.User']"}), + 'uid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}) + }, + 'gracedb.eventlog': { + 'Meta': {'ordering': "['-created']", 'object_name': 'EventLog'}, + 'comment': ('django.db.models.fields.TextField', [], {}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Event']"}), + 'filename': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.User']"}) + }, + 'gracedb.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'managers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['gracedb.User']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + 'gracedb.label': { + 'Meta': {'object_name': 'Label'}, + 'defaultColor': ('django.db.models.fields.CharField', [], {'default': "'black'", 'max_length': '20'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}) + }, + 'gracedb.labelling': { + 'Meta': {'object_name': 'Labelling'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.User']"}), + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Event']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'label': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['gracedb.Label']"}) + }, + 'gracedb.multiburstevent': { + 'Meta': {'ordering': "['-id']", 'object_name': 'MultiBurstEvent', '_ormbases': ['gracedb.Event']}, + 'amplitude': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'bandwidth': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'central_freq': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'confidence': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'event_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['gracedb.Event']", 'unique': 'True', 'primary_key': 'True'}), + 'false_alarm_rate': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ifos': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '20'}), + 'ligo_angle': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ligo_angle_sig': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ligo_axis_dec': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'ligo_axis_ra': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'peak_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'peak_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'snr': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'start_time': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'start_time_ns': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}) + }, + 'gracedb.tag': { + 'Meta': {'object_name': 'Tag'}, + 'displayName': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}), + 'eventlogs': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['gracedb.EventLog']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'gracedb.user': { + 'Meta': {'ordering': "['name']", 'object_name': 'User'}, + 'dn': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'principal': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'unixid': ('django.db.models.fields.CharField', [], {'max_length': '25'}) + } + } + + complete_apps = ['gracedb'] \ No newline at end of file diff --git a/gracedb/models.py b/gracedb/models.py index 485b11c1ed509631b32669ee82420c402b264f55..e7c3afea9911fd1fd5ef80f43b0e404ada77f659 100644 --- a/gracedb/models.py +++ b/gracedb/models.py @@ -173,6 +173,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"] @@ -193,6 +212,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) @@ -240,21 +269,26 @@ class MultiBurstEvent(Event): ligo_angle = models.FloatField(null=True) ligo_angle_sig = models.FloatField(null=True) -## Slots (user-defined event attributes) - -class Slot(models.Model): - """Slot Model""" - # Does the slot need to have a submitter column? - class Meta: - unique_together = (('event', 'name')) - event = models.ForeignKey(Event) - name = models.CharField(max_length=100) - value = models.CharField(max_length=100) +## Tags (user-defined log message attributes) +class Tag(models.Model): + """Tag 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) - # In case the slot value is not a filename, this will just return None. - def fileurl(self): - if self.value: - return reverse('file', args=[self.event.graceid(), self.value]) + def __unicode__(self): + if self.displayName: + return self.displayName else: - return None + 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()] diff --git a/gracedb/templatetags/logtags.py b/gracedb/templatetags/logtags.py new file mode 100644 index 0000000000000000000000000000000000000000..c19075255941e756ca736c0ad6b0386a404267ed --- /dev/null +++ b/gracedb/templatetags/logtags.py @@ -0,0 +1,69 @@ + +from django import template +from django.utils.encoding import force_unicode +from django.utils.safestring import mark_safe +from ..models import Tag, EventLog + +register = template.Library() + +@register.filter("getLogsForTag") +def getLogsForTag(event,name=None): + if event is None: + return None + try: + if name: + return event.getLogsForTag(name) + else: + return None + except: + # Either there is no such tag or something went wrong. + # In either case, we want the template to just ignore it. + return None + +@register.filter("tagUnicode") +def tagUnicode(tag): + return unicode(tag); + +@register.filter("logsForTagHaveImage") +def logsForTagAllHaveImages(event,name=None): + if event is None: + return None + try: + if name: + loglist = event.getLogsForTag(name) + # Start by assuming no images + bool_rv = False + for log in loglist: + # If any of the log messages do have an image + # attached, we flip the return value to true. + if log.hasImage(): + bool_rv = True + break + return bool_rv + else: + return None + except: + # Either there is no such tag or something went wrong. + # In either case, we want the template to just ignore it. + return None + +@register.filter("logsForTagHaveText") +def logsForTagHaveText(event,name=None): + if event is None: + return None + try: + if name: + loglist = event.getLogsForTag(name) + bool_rv = False + for log in loglist: + if not log.hasImage(): + bool_rv = True + break + return bool_rv + else: + return None + except: + # Either there is no such tag or something went wrong. + # In either case, we want the template to just ignore it. + return None + diff --git a/gracedb/templatetags/slot.py b/gracedb/templatetags/slot.py deleted file mode 100644 index 63e7b16827c8d94711d2563cde26fb788f473ebd..0000000000000000000000000000000000000000 --- a/gracedb/templatetags/slot.py +++ /dev/null @@ -1,21 +0,0 @@ - -from django import template -from django.utils.encoding import force_unicode -from django.utils.safestring import mark_safe -from ..models import Slot, EventLog -register = template.Library() - -@register.filter("slot") -def slot(event,name=None): - if event is None: - return None - try: - if name: - return Slot.objects.filter(event=event).filter(name__exact=name)[0] - else: - return Slot.objects.filter(event=event) - except: - # Either there is no such slot or something went wrong. - # In either case, we want the template to just ignore it. - return None - diff --git a/gracedb/urls.py b/gracedb/urls.py index a5839868cc51c5f263f8fd719a6634d81c5f9d4a..959ceb7a04eb0ced59096056d45a7bc4d8debca0 100644 --- a/gracedb/urls.py +++ b/gracedb/urls.py @@ -15,6 +15,7 @@ urlpatterns = patterns('gracedb.views', url (r'^(?P<graceid>[GEHT]\d+)$', 'view', name="view2"), url (r'^(?P<graceid>[GEHT]\d+)/files/(?P<filename>.*)$', download, name="file"), url (r'^(?P<graceid>[GEHT]\d+)/log/(?P<num>([\d]*|preview))$', 'logentry', name="logentry"), + url (r'^(?P<graceid>[GEHT]\d+)/log/(?P<num>\d+)/tag/(?P<tagname>\w+)$', 'taglogentry', name="taglogentry"), # (r'^view/(?P<uid>[\w\d]+)', 'view'), diff --git a/gracedb/urls_rest.py b/gracedb/urls_rest.py index d915fe9e4996cdfb41625b04da04a7d4318aa751..828bfec477bde347f8b5592539e343a79e6f9209 100644 --- a/gracedb/urls_rest.py +++ b/gracedb/urls_rest.py @@ -5,7 +5,10 @@ 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 EventSlot +from gracedb.api import TagList +# from gracedb.api import TagDetail +from gracedb.api import EventTagList, EventTagDetail +from gracedb.api import EventLogTagList, EventLogTagDetail from gracedb.api import Files, FileMeta from gracedb.api import EventNeighbors, EventLabel @@ -27,6 +30,21 @@ 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'), + # XXX unclear what the tag detail resource should be. + #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>.+)?$', @@ -40,11 +58,6 @@ urlpatterns = patterns('gracedb.api', url (r'^events/(?P<graceid>\w[\d]+)/labels/(?P<label>.+)?$', EventLabel.as_view(), name="labels"), - # Event Slots - # events/{graceid}/slot/[{slotname}] - url (r'^events/(?P<graceid>[GEHT]\d+)/slot/(?P<slotname>.+)?$', - EventSlot.as_view(), name="slot"), - # Event Neighbors # events/{graceid}/neighbors/[?delta=(N|(N,N))] url (r'^events/(?P<graceid>\w[\d]+)/neighbors/$', diff --git a/gracedb/views.py b/gracedb/views.py index 3961a80c75021cc03cc3644aa77552e8f1d09975..09b9b3a8ff2511ab6322f214a4997e6eb58bacbb 100644 --- a/gracedb/views.py +++ b/gracedb/views.py @@ -10,7 +10,7 @@ from django.utils.safestring import mark_safe from django.views.generic.list_detail import object_detail, object_list -from models import Event, Group, EventLog, Labelling, Label, User +from models import Event, Group, EventLog, Labelling, Label, User, Tag from models import CoincInspiralEvent from models import MultiBurstEvent from forms import CreateEventForm, EventSearchForm, SimpleSearchForm @@ -29,6 +29,7 @@ from django.conf import settings from templatetags.scientific import scientific from buildVOEvent import buildVOEvent, submitToSkyalert +import logging # XXX This should be configurable / moddable or something MAX_QUERY_RESULTS = 1000 @@ -443,6 +444,7 @@ def sanitize_html(data): def logentry(request, graceid, num=None): + logger = logging.getLogger(__name__) try: event = Event.getByGraceid(graceid) except Event.DoesNotExist: @@ -452,6 +454,28 @@ def logentry(request, graceid, num=None): elog = EventLog(event=event, issuer=request.ligouser) elog.comment = request.POST.get('comment') or request.GET.get('comment') elog.save() + logger.debug("just saved log entry") + tagname = request.POST.get('tagname') + logger.debug("tagname = %s" % tagname) + if tagname: + # Look for the tag. If it doesn't already exist, create it. + try: + tag = Tag.objects.filter(name=tagname)[0] + except: + displayName = request.POST.get('displayName') + logger.debug("disp name = %s" % displayName) + tag = Tag(name=tagname, displayName=displayName) + tag.save() + logger.debug("just saved tag") + + tag.eventlogs.add(elog) + # Create a log entry to document the tag creation. + num = elog.getN() + msg = "Tagged message %s: %s " % (num, tagname) + tlog = EventLog(event=event, + issuer=request.ligouser, + comment=msg) + tlog.save() else: try: elog = event.eventlog_set.order_by('created').all()[int(num)] @@ -466,6 +490,8 @@ def logentry(request, graceid, num=None): rv['issuer'] = elog.issuer.name rv['created'] = elog.created.isoformat() rv['comment'] = elog.comment + if tagname: + rv['tagname'] = tagname return HttpResponse(json.dumps(rv), content_type="application/json") @@ -537,6 +563,7 @@ def view(request, graceid): context['nearby'] = [(event.gpstime - a.gpstime, event) for event in a.neighbors()] context['skyalert_authorized'] = skyalert_authorized(request) + context['blessed_tags'] = settings.BLESSED_TAGS return render_to_response( [ 'gracedb/event_detail_{0}.html'.format(a.analysisType), 'gracedb/event_detail.html'], @@ -993,3 +1020,53 @@ def latest(request): context, context_instance=RequestContext(request)) +#----------------------------------------------------------------------------------- +# For tags. A new view function. We need this because the API one would want users +# to have certs stored in their browser. +#----------------------------------------------------------------------------------- + +def taglogentry(request, graceid, num, tagname): + try: + event = Event.getByGraceid(graceid) + eventlog = event.eventlog_set.order_by("created").all()[int(num)] + except: + # Either the event or the log does not exist. + raise Http404 + + if request.method == "POST": + try: + # Has this tag-eventlog relationship already been created? + tag = eventlog.tag_set.filter(name=tagname)[0] + msg = "Log already has tag %s" % tagname + return HttpResponse(msg, content_type="text") + except: + # Look for the tag. If it doesn't already exist, create it. + try: + tag = Tag.objects.filter(name=tagname)[0] + except: + displayName = request.POST['displayName'] + 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 " % (num, tagname) + logentry = EventLog(event=event, + issuer=request.ligouser, + comment=msg) + logentry.save() + else: + # We will only allow PUT here. Anything else is a bad request: 400 + return HttpResponseBadRequest + + # Hopefully, this will only ever be called form inside a script. Just in case... + if not request.is_ajax(): + return HttpResponseRedirect(reverse(view, args=[graceid])) + + # no need for a JSON response. + msg = "Successfully applied tag %s to log message %s." % (tagname, num) + msg = msg + " Refresh to see chages (if any) to the presentation." + return HttpResponse(msg, content_type="text") + diff --git a/settings/default.py b/settings/default.py index 08d7db56cd86e95bd58108f84cc30dd5d782fad6..4e6d3484c6e5a4c94a571e228cb9e44f6979dc87 100644 --- a/settings/default.py +++ b/settings/default.py @@ -37,6 +37,15 @@ XMPP_ALERT_CHANNELS = [ 'test_grb', ] +BLESSED_TAGS = [ + 'psd', + 'sky_loc', + 'strain', + 'tfplots', + 'sig_info', + 'audio', + ] + DATABASES = { 'default' : { 'NAME' : 'gracedb', diff --git a/static/css/style.css b/static/css/style.css index 268f6f7f9d010987cbcaf2fe4eadebac91d2996f..76f5aaf68f16c7035bd9566681124b00821f54be 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -9,6 +9,45 @@ html, body { table.event th {padding:3px;border:none;text-align:center;vertical-align:bottom;} table.event {border-bottom:1px solid gray;} +/* Branson added */ +table.event {width:100%} + +table.analysis_specific {width:100%} + +.content-area { + margin-top: 10px; + margin-bottom: 30px; + margin-left: 15px; + margin-right: 15px; +} + +table.figures tr.figrow {text-align:center;} +table.figures {width:300px;height:270px;border:1px solid gray;} + +.tundra.eventDetail .pmTitlePaneClass .dijitOpen .dijitArrowNode { + background-repeat: no-repeat; + height: 14px; + width: 14px; +} + +.tundra.eventDetail .pmTitlePaneClass .dijitClosed .dijitArrowNode { + background-repeat: no-repeat; + height: 14px; + width: 14px; +} + +.tundra.eventDetail .pmTitlePaneClass .dijitClosed .dijitArrowNode { + background-image: url('/branson-static/images/plusButton.gif'); + background-position: 0px 0px; +} + +.tundra.eventDetail .pmTitlePaneClass .dijitOpen .dijitArrowNode { + background-image: url('/branson-static/images/minusButton.gif'); + background-position: 0px 0px; +} + + +/* End Branson */ td, th {padding:3px;border:none;} tr th {text-align:left;background-color:#f0f0f0;color:#333;} diff --git a/static/images/minusButton.gif b/static/images/minusButton.gif new file mode 100644 index 0000000000000000000000000000000000000000..48d5172470f250383e7696c7b61c727b9df06eb0 Binary files /dev/null and b/static/images/minusButton.gif differ diff --git a/static/images/plusButton.gif b/static/images/plusButton.gif new file mode 100644 index 0000000000000000000000000000000000000000..103c021daa0668c5b0ba53ac0bace43eb1f1f7ee Binary files /dev/null and b/static/images/plusButton.gif differ diff --git a/templates/base.html b/templates/base.html index f9eec575d1aba079a5c7d27d8730603ecf10cb99..dd0b46c33bbb695399ebaa2467ecbf2c5ae88b13 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,7 +2,7 @@ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> - <link rel="stylesheet" href="/gracedb-static/css/style.css" /> + <link rel="stylesheet" href="/branson-static/css/style.css" /> <title>GraceDb | {% block title %}{% endblock %}</title> <!-- START TESTING --> <script type="text/javascript"> diff --git a/templates/gracedb/event_detail.html b/templates/gracedb/event_detail.html index aa948a79a168fe73d48dd8b6cf076b6596e425c4..c09d576e378d2cc6eb45e5953affdf1869014cee 100644 --- a/templates/gracedb/event_detail.html +++ b/templates/gracedb/event_detail.html @@ -2,27 +2,32 @@ {% load timeutil %} {% load scientific %} {% load sanitize_html %} -{% load slot %} - +{% load logtags %} {% block title %}View {{ object.graceid }}{% endblock %} {% block heading %}{% endblock %} -{% block bodyattrs %}class="tundra"{% endblock %} +{% block bodyattrs %}class="tundra eventDetail"{% endblock %} {% block jscript %} {# XXX http/https depending on this pages' protocol #} <script type="text/javascript" src="https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script> - <script src="http{% if request.is_secure %}s{% endif %}://ajax.googleapis.com/ajax/libs/dojo/1.7.1/dojo/dojo.js" type="text/javascript"></script> + <script src="http{% if request.is_secure %}s{% endif %}://ajax.googleapis.com/ajax/libs/dojo/1.8.1/dojo/dojo.js" type="text/javascript"></script> <script src="{{MEDIA_URL}}js/labeltips.js" type="text/javascript"></script> <link rel="stylesheet" href="{{MEDIA_URL}}css/labeltips.css"> <script type="text/javascript"> - var bb = null; - - require(["dijit/Editor", + require(["dojo/dom", + "dojo/on", + "dojo/request", + "dojo/dom-form", + "dijit/Editor", "dojox/editor/plugins/Save", "dojox/editor/plugins/Preview", "dijit/form/Button", + "dijit/TitlePane", + "dojox/layout/ScrollPane", + "dojo/store/Memory", + "dijit/form/ComboBox", "dojox/image/LightboxNano", "dijit/_editor/plugins/TextColor", "dijit/_editor/plugins/LinkDialog", @@ -30,19 +35,26 @@ "dijit/_editor/plugins/NewPage", "dijit/_editor/plugins/FullScreen", "dojo/parser", - "dojo/domReady!"], function (Editor, Save, Preview, Button) { + "dojo/domReady!"], function (dom, on, request, domForm, Editor, Save, Preview, + Button, TitlePane, ScrollPane, Memory, ComboBox) { dojo.parser.parse(); - //var newlogdiv = dojo.byId("newlogdiv"); - var logtitle = dojo.byId("logmessagetitle"); - var logtext = dojo.byId("newlogtext"); + //var newlogdiv = dom.byId("newlogdiv"); + var logtitle = dom.byId("logmessagetitle"); + var logtext = dom.byId("newlogtext"); + + var editor_div = dom.byId("editor"); + var preview_div = dom.byId("previewer"); - var editor_div = dojo.byId("editor"); - var preview_div = dojo.byId("previewer"); + // A pane holder for the form that will tag new log messages. + // I need it up here because we're going to integrate it with the + // editor components. + var newLogTag_pane_holder = dom.byId("newLogTag_pane_holder"); dojo.style(preview_div, { 'display':'none'}); dojo.style(editor_div, { 'display':'none'}); + dojo.style(newLogTag_pane_holder, { 'display':'none'}); var button_element = dojo.create('button'); dojo.place(button_element, logtitle, "right"); @@ -52,12 +64,14 @@ onClick: function(){ if (this.state == 'add') { dojo.style(editor_div, {'display':'block'}); + dojo.style(newLogTag_pane_holder, {'display':'block'}); button.set('label', "Cancel Log Entry"); button.set('state', 'cancel'); editor.focus(); } else { dojo.style(editor_div, {'display':'none'}); + dojo.style(newLogTag_pane_holder, {'display':'none'}); dojo.style(preview_div, {'display':'none'}); button.set('label', "Add Log Entry"); button.set('state', 'add'); @@ -66,9 +80,6 @@ }, }, button_element); - - //dojo.style(newlogdiv, {'display':'none'}); - var savebutton = new Save({ url: "{% url logentry object.graceid "" %}", onSuccess: function (resp, ioargs) { @@ -82,15 +93,19 @@ alert("o hai " + error); }, save: function(postdata) { - var postArgs = { + var newTagName = domForm.fieldToObject("newTagSelect"); + var newTagDispName = domForm.fieldToObject("newTagDispName"); + var postArgs = { url: this.url, - content: { comment: postdata }, + content: { comment: postdata, tagname: newTagName, displayName: newTagDispName }, handleAs: "json" }; this.button.set("disabled", true); var deferred = dojo.xhrPost(postArgs); deferred.addCallback(dojo.hitch(this, this.onSuccess)); deferred.addErrback(dojo.hitch(this, this.onError)); + // Call whatever function is necessary to attach the tag + // or add to the postdata and handle in the django view? } }); @@ -107,11 +122,133 @@ } }); + var editor = new Editor({ extraPlugins : ['foreColor','hiliteColor','|','createLink', 'insertImage','fullscreen','viewsource','newpage', '|', previewbutton, savebutton] }, editor_div); + // If we have blessed tags *and* some log messages have been tagged, + // we will want some TitlePanes. +{% if blessed_tags and object.getAvailableTags %} + var pane_holder = dom.byId("pane_holder"); + var panes = new Array(); + var titles = new Array(); + {% for tag in object.getAvailableTags %} + {% if tag.name in blessed_tags %} + panes.push(dom.byId("{{tag.name}}")); + titles.push("{{tag|tagUnicode}}"); + {% endif %} + {% endfor %} + + for (var i = 0; i<panes.length; i++) { + tp = new dijit.TitlePane({title:titles[i], + content:panes[i].innerHTML, + open:true}); + pane_holder.appendChild(tp.domNode); + dojo.destroy(panes[i]); + }; + + // Handle event log seperately. It will be closed by default. + var event_log = dom.byId("event_log"); + tp = new dijit.TitlePane({title:"Full Event Log", + content:event_log.innerHTML, + open:false}); + pane_holder.appendChild(tp.domNode); + dojo.destroy(event_log); + + // Now, deal with the silly figure containers. + var figure_container_ids = new Array(); + {% for tag in object.getAvailableTags %} + {% if tag.name in blessed_tags %} + if (dom.byId("{{tag.name}}_figure_container")) { + figure_container_ids.push("{{tag.name}}_figure_container"); + } + {% endif %} + {% endfor %} + var figure_scrollpanes = new Array(); + for (var i = 0; i<figure_container_ids.length; i++) { + figure_scrollpanes.push(new dojox.layout.ScrollPane({ + orientation:"horizontal", + style:"overflow:hidden;"}, + figure_container_ids[i])); + } +{% endif %} {# Endif for getAvailableTags and blessed_tags #} + + // Set up infrastructure for tagging log messages. +{% if blessed_tags %} + var blessedTagStore = new Memory({ + data: [ + {% for bt in blessed_tags %} + {name:"{{bt}}"}, + {% endfor %} + ] + }); +{% endif %} + + // Panes for the tagging forms. + var newLogTag = dom.byId("newLogTag"); + var existingLogTag = dom.byId("existingLogTag"); + var existingLogTag_pane_holder = dom.byId("existingLogTag_pane_holder"); + + tp_new = new dijit.TitlePane({title:"Tag this message", + content:newLogTag.innerHTML, + open:false}); + newLogTag_pane_holder.appendChild(tp_new.domNode); + dojo.destroy(newLogTag); + + tp_existing = new dijit.TitlePane({title:"Add tag", + content:existingLogTag.innerHTML, + open:false}); + existingLogTag_pane_holder.appendChild(tp_existing.domNode); + dojo.destroy(existingLogTag); + + // Form for tagging new log messages. + var cb1 = new ComboBox({ + id: "newTagSelect", + name: "newTagSelect", + value: "insert tag here", +{% if blessed_tags %} + store: blessedTagStore, + searchAttr: "name" +{% endif %} + }, "newTagSelect"); + + // Form for tagging existing log messages. + var cb2 = new ComboBox({ + id: "existingTagSelect", + name: "existingTagSelect", + value: "None", +{% if blessed_tags %} + store: blessedTagStore, + searchAttr: "name" +{% endif %} + }, "existingTagSelect"); + + var form = dom.byId('existingTagForm'); + on(form, "submit", function(evt){ + evt.stopPropagation(); + evt.preventDefault(); + + var n = domForm.fieldToObject("n"); + var tagName = domForm.fieldToObject("existingTagSelect"); + var tagDispName = domForm.fieldToObject("existingTagDispName"); + var tagUrl = "{% url taglogentry object.graceid "000" "temp" %}" + tagUrl = tagUrl.replace("000",n); + tagUrl = tagUrl.replace("temp",tagName); + + request.post(tagUrl, { + data: {displayName: tagDispName} + }).then( + function(text){ + alert(text); + // You may want to force the page to reload. + //location.reload(true); + }, + function(error){alert(error);} + ); + }); + }); </script> @@ -157,9 +294,7 @@ {% block content %} <p>{{ message }}</p> -<table> -<tr><td valign="top"> - +<div class="content-area"> <h3> Basic Info </h3> <table class="event"> @@ -208,14 +343,17 @@ </tr> </table> +</div> +<div class="content-area"> {# Analysis-specific attributes #} {% block analysis_specific %} {# This block is empty in the base event_detail template #} {% endblock %} +</div> {% if nearby %} - <p/> + <div id="neighbors" class="content-area"> <h3>Neighbors</h3> <table class="event"> <tr> @@ -255,6 +393,99 @@ </tr> {% endfor %} </table> + </div> +{% endif %} + +<div id="pane_holder" class="content-area"> + {# XXX Hacky #} + {# With the title here, people will know that all of the things in the #} + {# panes are log messages #} +{% if object.getAvailableTags %} + <h3 id="logmessagetitle">Event Log Messages</h3> + <div id="newLogTag"> + <p> Use this form to apply a tag to your new log entry. Choose a tag + name from the dropdown menu or enter a new one. If you are + creating a new tag, please also provide a display name. </p> + <form> + Tag name <input id="newTagSelect" name="newTagSelect" /> + Display name <input type="text" id="newTagDispName" name="newTagDispName" /> + </form> + </div> + <div id="newLogTag_pane_holder" class="pmTitlePaneClass"></div> + <div id="previewer"></div> + <div id="editor"></div> +{% endif %} + +{# Loop over the available tags. #} +{% if object.getAvailableTags %} + {% for tag in object.getAvailableTags %} + {% if tag.name in blessed_tags %} + {# Create a div to hold the pane innerHTML for this tag #} + <div id="{{tag.name}}"> + + {# A section for the images #} + {% if object|logsForTagHaveImage:tag.name %} + <!-- figure container table is inside a scrolling div --> + <div id="{{tag.name}}_figure_container"> + <table class="figure_container"> + <tr> + {% for log in object|getLogsForTag:tag.name %} + {% if log.hasImage %} + <td> + <table class="figures"> + <!-- A row for the images themselves --> + <tr class="figrow"> + <td> <a href="{{ log.fileurl }}" dojoType="dojox.image.LightboxNano"><img height="180" src="{{ log.fileurl }}"></a> + </td> + </tr> + <!-- A row for the captions --> + <tr> + <td> {{log.comment|sanitize}} + {% if log.fileurl %} + <a href="{{log.fileurl}}">{{log.filename}}.</a> + {% endif %} + Submitted by {{log.issuer}} on {{log.created}} + </td> + </tr> + </table> + </td> + {% endif %} + {% endfor %} + </tr> + </table> + </div> + {% endif %} + + {# A section for the text-only captions #} + {% if object|logsForTagHaveText:tag.name %} + {# Set up regular log message table #} + <table class="event"> + <tr> + <th>No.</th> + <th>{{ "logtime"|timeselect:"utc" }} Log Entry Created</th> + <th>Submitter</th> + <th>Comment</th> + </tr> + {% for log in object|getLogsForTag:tag.name %} + {% if not log.hasImage %} + <tr class="{% cycle 'odd' 'even'%}"> + <td>{{log.getN}}</td> + <td>{{log.created|multiTime:"logtime"}}</td> + <td>{{log.issuer}}</td> + <td>{{log.comment|sanitize}} + {% if log.fileurl %} + <a href="{{log.fileurl}}">{{log.filename}}</a> + {% endif %} + </td> + </tr> + {% endif %} + {% endfor %} + </table> + {% endif %} + + </div> + {% endif %} {# endif the tag is blessed #} + {% endfor %} {% endif %} <noscript> @@ -266,15 +497,44 @@ </noscript> - <p/> - <h3 id="logmessagetitle">Event Log Messages</h3> - + <div id="event_log"> + {# XXX Hacky. We don't want the title here if we're doing panes. #} + {# And we only do panes if there are available tags. #} +{% if not object.getAvailableTags %} + <h3 id="logmessagetitle">Event Log Messages</h3> + <div id="newLogTag"> + <p> Use this form to apply a tag to your new log entry. Choose a tag + name from the dropdown menu or enter a new one. If you are + creating a new tag, please also provide a display name. </p> + <form> + Tag name <input id="newTagSelect" name="newTagSelect" /> + Display name <input type="text" id="newTagDispName" name="newTagDispName" /> + </form> + </div> + <div id="newLogTag_pane_holder" class="pmTitlePaneClass"></div> <div id="previewer"></div> <div id="editor"></div> +{% endif %} {% if object.eventlog_set.count %} + {% if blessed_tags %} + <div id="existingLogTag"> + <p> Use this form to tag an existing log message. Choose a tag name from the + dropdown menu or enter a new one. If you are creating a new tag, please + also provide a display name. + <form id="existingTagForm"> + Log No. <input id="n" name="n" type="text" size="4" /> + Tag name <input id="existingTagSelect" name="existingTagSelect" /> + Display name <input type="text" id="existingTagDispName" name="existingTagDispName"/> + <input type="submit" name="submit" value="Submit" /> </p> + </form> + </div> + <div id="existingLogTag_pane_holder" class="pmTitlePaneClass"></div> + {% endif %} + <table class="event"> <tr> + <th>No.</th> <th>{{ "logtime"|timeselect:"utc" }} Log Entry Created</th> <th>Submitter</th> <th>Comment</th> @@ -282,6 +542,7 @@ </tr> {% for log in object.eventlog_set.iterator %} <tr class="{% cycle 'odd' 'even'%}"> + <td>{{log.getN}} <td>{{log.created|multiTime:"logtime"}}</td> <td>{{log.issuer}}</td> <td>{{log.comment|sanitize}} @@ -298,32 +559,7 @@ {% endfor %} </table> {% endif %} + </div> -</td> -<td rowspan="2" valign="top"> - -{% if eventdesc %} -<h3>Event Log File</h3> - {{ eventdesc }} - <br/> -{% endif %} - -{% if userdesc %} - <br/> -<h3>User Info</h3> - {{ userdesc }} - <br/> -{% endif %} - -<p/> -<h3>Available Slots</h3> -{% if not object|slot %} - None. -{% else %} - {% for slot in object|slot %} - <a href="{{slot.fileurl}}">{{ slot.name }}</a> <br/> - {% endfor %} -{% endif %} - -</td></tr></table> +</div> <!-- End of pane_holder div --> {% endblock %} diff --git a/templates/gracedb/event_detail_CWB.html b/templates/gracedb/event_detail_CWB.html index e91ae26ad951849a519617374f223b5a543d31ee..de184254918e290919a0d69ea6fdde66fff6ed64 100644 --- a/templates/gracedb/event_detail_CWB.html +++ b/templates/gracedb/event_detail_CWB.html @@ -4,10 +4,9 @@ {# Analysis-specific attributes for a cWB event#} {% block analysis_specific %} -<p/> <h3>Analysis-Specific Attributes</h3> -<table> <tbody> +<table class="analysis_specific"> <tbody> <tr> <td> <table class="event"> <tbody> <!-- Note: commenting out ifos so that we'll have a multiple of three --> diff --git a/templates/gracedb/event_detail_HM.html b/templates/gracedb/event_detail_HM.html index 735c28dbfddfa175ac660ea6cb8204d642bd3fec..d1f0f693c88452194a4ec483d96f7d5782f5243b 100644 --- a/templates/gracedb/event_detail_HM.html +++ b/templates/gracedb/event_detail_HM.html @@ -1,10 +1,32 @@ {% extends "gracedb/event_detail.html" %} -{# Analysis-specific attributes for a LowMass event#} -{% block analysis_specific %} - <p/> -<h3>Analysis-specific attributes</h3> +{% load scientific %} +{# Analysis-specific attributes for an LM event#} +{% block analysis_specific %} +<h3>Analysis-Specific Attributes</h3> +<table class="analysis_specific"> <tbody> + <tr> + <td> <table class="event"> <tbody> + <tr> <th> ifos </th> <td> {{object.ifos}} </td> </tr> + <tr> <th> end_time </th> <td> {{object.end_time}} </td> </tr> + <tr> <th> end_time_ns </th> <td> {{object.end_time_ns}} </td> </tr> + </tbody></table> + </td> + <td> <table class="event"> <tbody> + <tr> <th> mass </th> <td> {{object.mass|floatformat:"-4"}} </td> </tr> + <tr> <th> mchirp </th> <td> {{object.mchirp|floatformat:"-4"}} </td> </tr> + <tr> <th> minimum_duration </th> <td> {{object.minimum_duration|scientific}} </td> </tr> + </tbody></table> + </td> + <td> <table class="event"> <tbody> + <tr> <th> snr </th> <td> {{object.snr|floatformat:"-4"}} </td> </tr> + <tr> <th> false_alarm_rate </th> <td> {{object.false_alarm_rate|floatformat:"-4"}} </td> </tr> + <tr> <th> combined_far </th> <td> {{object.combined_far|scientific}} </td> </tr> + </tbody></table> + </td> + </tr> +</tbody> </table> {% endblock %} diff --git a/templates/gracedb/event_detail_LM.html b/templates/gracedb/event_detail_LM.html index 3a1e4dfeb5f7e217b731968383b5563e42ce6fc5..d1f0f693c88452194a4ec483d96f7d5782f5243b 100644 --- a/templates/gracedb/event_detail_LM.html +++ b/templates/gracedb/event_detail_LM.html @@ -4,10 +4,9 @@ {# Analysis-specific attributes for an LM event#} {% block analysis_specific %} -<p/> <h3>Analysis-Specific Attributes</h3> -<table> <tbody> +<table class="analysis_specific"> <tbody> <tr> <td> <table class="event"> <tbody> <tr> <th> ifos </th> <td> {{object.ifos}} </td> </tr> @@ -30,5 +29,4 @@ </tr> </tbody> </table> - {% endblock %} diff --git a/templates/gracedb/event_detail_MBTA.html b/templates/gracedb/event_detail_MBTA.html index 735c28dbfddfa175ac660ea6cb8204d642bd3fec..81ba7576fa013ec7644aa556f4ce01c85cb5dd3b 100644 --- a/templates/gracedb/event_detail_MBTA.html +++ b/templates/gracedb/event_detail_MBTA.html @@ -3,8 +3,7 @@ {# Analysis-specific attributes for a LowMass event#} {% block analysis_specific %} <p/> -<h3>Analysis-specific attributes</h3> - - +<h3>Event Log File</h3> +{{eventdesc}} {% endblock %}