From febe41d782707c0485ef0078e83e7580186124a4 Mon Sep 17 00:00:00 2001 From: Tanner Prestegard <tanner.prestegard@ligo.org> Date: Thu, 12 Apr 2018 14:20:57 -0500 Subject: [PATCH] basic web view for superevents --- gracedb/core/forms.py | 42 +++++ gracedb/static/css/style.css | 6 + gracedb/superevents/forms.py | 60 +++++++ .../templates/log_create_form.html | 12 ++ gracedb/superevents/templates/superevent.html | 160 ++++++++++++++++++ gracedb/superevents/views.py | 133 +++++++++++++++ 6 files changed, 413 insertions(+) create mode 100644 gracedb/core/forms.py create mode 100644 gracedb/superevents/forms.py create mode 100644 gracedb/superevents/templates/log_create_form.html create mode 100644 gracedb/superevents/templates/superevent.html create mode 100644 gracedb/superevents/views.py diff --git a/gracedb/core/forms.py b/gracedb/core/forms.py new file mode 100644 index 000000000..417385f67 --- /dev/null +++ b/gracedb/core/forms.py @@ -0,0 +1,42 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +class ModelFormUpdateMixin(forms.ModelForm): + """ + ModelForm mixin which provides the capability to update an existing model + object with input data which is incomplete - i.e., doesn't contain all + fields required by the form. + + Usage: + # For a model with attributes 'attr1' and 'attr2 and a form that + # normally requires both attributes + data_dict = {'attr1': 'example'} + ExampleForm(data_dict, instance=model_instance) + """ + + def __init__(self, *args, **kwargs): + super(ModelFormUpdateMixin, self).__init__(*args, **kwargs) + + # If instance is provided, populate missing data + if self.instance.pk is not None: + self.populate_missing_data() + + def get_instance_data(self): + # Should be overridden by concrete derived classes + return NotImplemented + + def populate_missing_data(self): + """ + Populate missing data fields from model instance. + + NOTE: the actual data dictionary is populated, not initial data, + because missing fields in the input data would still be ignored. + """ + if self.instance.pk is None: + self.add_error(None, _('Model instance not found')) + + # Insert instance data for missing fields only + instance_data = self.get_instance_data() + for key in self.fields.keys(): + if not self.data.has_key(key) and instance_data[key]: + self.data[key] = instance_data[key] diff --git a/gracedb/static/css/style.css b/gracedb/static/css/style.css index 619290102..849767d32 100644 --- a/gracedb/static/css/style.css +++ b/gracedb/static/css/style.css @@ -32,6 +32,12 @@ table.event {width:100%} /* Tanner added - centering columns in multi-column tables */ table.eventmulti td {text-align:center;} +/* superevent info table */ +.superevent th {padding: 5px 10px 5px 10px;border:none;text-align:center;vertical-align:bottom;} +.superevent td {padding:10px;border:none;vertical-align:bottom;} +.superevent {border-bottom:1px solid gray;} +/* #table.superevent {width:100%} */ + table.analysis_specific_lm th {padding:3px;border:none;text-align:center;vertical-align:bottom;} table.analysis_specific_lm {border-bottom:1px solid gray;} diff --git a/gracedb/superevents/forms.py b/gracedb/superevents/forms.py new file mode 100644 index 000000000..2c1c10a34 --- /dev/null +++ b/gracedb/superevents/forms.py @@ -0,0 +1,60 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .models import Superevent, Log +from .utils import create_log +from core.forms import ModelFormUpdateMixin +from core.vfile import VersionedFile +from events.models import Event + +import os + +import logging +logger = logging.getLogger(__name__) + + +class LogCreateForm(forms.ModelForm): + # This field is used to get file upload, but is not actually + # part of the Log model + data_file = forms.FileField(label="Data file", required=False) + + class Meta: + model = Log + fields = ['issuer', 'superevent', 'filename', 'file_version', + 'data_file', 'comment'] + + def __init__(self, *args, **kwargs): + super(LogCreateForm, self).__init__(*args, **kwargs) + + # Make certain fields hidden in the web view. We will either specify + # the value in the initial view or when we handle the POST data. + self.fields['filename'].widget = forms.HiddenInput() + self.fields['file_version'].widget = forms.HiddenInput() + self.fields['issuer'].widget = forms.HiddenInput() + self.fields['superevent'].widget = forms.HiddenInput() + + def save(self, commit=True): + # Get data file (if present) + create_log_args = self.cleaned_data.copy() + create_log_args['issue_alert'] = True + return create_log(**create_log_args) + #return create_log(**self.cleaned_data, issue_alert=True) + #data_file = self.cleaned_data.pop('data_file', None) + + ## Inherited save from ModelForm + #obj = super(LogCreateForm, self).save(commit) + + ## Do other stuff with data file + #if data_file: + # filepath = os.path.join(obj.superevent.datadir, + # self.cleaned_data['filename']) + # fdest = VersionedFile(filepath, 'w') + # for chunk in data_file.chunks(): + # fdest.write(chunk) + # fdest.close() + + # obj.file_version = fdest.version + # if commit: + # obj.save() + + #return obj diff --git a/gracedb/superevents/templates/log_create_form.html b/gracedb/superevents/templates/log_create_form.html new file mode 100644 index 000000000..47a7f8f37 --- /dev/null +++ b/gracedb/superevents/templates/log_create_form.html @@ -0,0 +1,12 @@ +<h2>Create log message</h2> + +<div style="padding-bottom: 20px"> +<form enctype="multipart/form-data" action="{% url "superevents:create-log" superevent.superevent_id %}" method="POST"> + <table> + {{ log_create_form.as_table }} + </table> + <input type="submit" value="Submit" /> +</form> +</div> + + diff --git a/gracedb/superevents/templates/superevent.html b/gracedb/superevents/templates/superevent.html new file mode 100644 index 000000000..38c344f7e --- /dev/null +++ b/gracedb/superevents/templates/superevent.html @@ -0,0 +1,160 @@ +{% extends "base.html" %} +{% load timeutil %} +{% load scientific %} +{% load sanitize_html %} +{% load logtags %} +{% block heading %}{% endblock %} +{% block bodyattrs %}class="tundra eventDetail"{% endblock %} + +{% block jscript %} +{% endblock %} + +{% block content %} + +TBD: +<ul style="padding-bottom: 30px;"> +<li>javascript update for labels</li> +<li>link for log message files, make sure to link to correct file version</li> +<li>event log tagging</li> +</ul> + +<div id='event_detail_content'> + +{% block superevent_info %} +<h2>Superevent Info</h2> +<table class="superevent"> + <tr> + <th>Superevent ID</th> + <th>Labels</th> + <th>Preferred Event</th> + <th>GW events</th> + <th>External events</th> + </tr> + <tr> + <td>{{ superevent.superevent_id }}</td> + <td>{% for labelling in superevent.labelling_set.all %} + <div style="color: {{ labelling.label.defaultColor }}"><b>{{ labelling.label.name }}</b></div> + {% endfor %} + </td> + + <td><a href="{% url "view" preferred_event.graceid %}">{{ preferred_event.graceid }}</a></td> + <td> + <div> + {% for graceid in internal_events %} + <a href="{% url "view" graceid %}">{{ graceid }}</a> + {% endfor %} + </div> + </td> + <td> + <div> + {% for graceid in external_events %} + <a href="{% url "view" graceid %}">{{ graceid }}</a> + {% endfor %} + </div> + </td> + </tr> +</table> +{% endblock %} + +<br /> +<br /> + +{% block basic_info %} +<h2>Preferred Event Info</h2> + +<table class="event"> + <tr> + <th valign="top">UID</th> + <th>Labels</th> + <th>Group</th> + <th>Pipeline</th> + <th>Search</th> + <th>Instruments</th> + <th> + <div id="basic_info_event_ts"></div> + <div> Event Time </div> + </th> + <th>FAR (Hz)</th> + <th>FAR (yr<sup>-1</sup>)</th> + <th>Links</th> + <th> + <div id="basic_info_created_ts"></div> + <div> Submitted </div> + </th> + </tr> + <tr> + <td><a href="{% url "view" preferred_event.graceid %}">{{ preferred_event.graceid }}</a></td> + <td> + {% for labelling in preferred_event.labelling_set.all %} + <span onmouseover="tooltip.show(tooltiptext('{{labelling.label.name}}', '{{labelling.creator.username}}', '{{labelling.created|utc}}', '{{labelling.label.description}}'));" onmouseout="tooltip.hide();" style="color: {{labelling.label.defaultColor}}">{{ labelling.label.name }}</span> + {% endfor %} + </td> + <td>{{ preferred_event.group.name }} </td> + <td>{{ preferred_event.pipeline.name }} </td> + <td>{{ preferred_event.search.name }} </td> + <td>{{ preferred_event.instruments }}</td> + <td>{% if preferred_event.gpstime %} + <!-- <span title="{{ preferred_event.gpstime|gpsdate }}">{{ preferred_event.gpstime }}</span> --> + {{ preferred_event.gpstime|multiTime:"gps" }} + {% endif %}</td> + {# NOTE: XXX Using event_far so it can be floored for external users. #} + <td>{% if far_is_upper_limit %} < {% endif %}{{ display_far|scientific }}</td> + <td>{% if far_is_upper_limit %} < {% endif %}{{ display_far_yr }}</td> + <td><a href="{{ preferred_event.weburl }}">Data</a></td> + <td>{{ preferred_event.created|multiTime:"created" }}</td> + </tr> +</table> +{% endblock %} +</div> + +<div class="content-area"> +{% if user_is_external %} +{# Analysis-specific attributes which can be exposed to external partners #} +{% block external_analysis_specific %} +{# Empty by default #} +{% endblock %} +{% else %} +{# Analysis-specific attributes #} +{% block analysis_specific %} +{# This block is empty in the base event_detail template #} +{% endblock %} +{% endif %} +</div> + +<!-- Form for creating new log messages --> +{% include "log_create_form.html" %} + +<!-- Set of log messages --> +<h2>Full event log</h2> +<div style="padding-bottom: 20px"> +<table> +<tr> + <th>No.</th> + <th>Log Entry Created</th> + <th>Submitter</th> + <th>Comment</th> + <th>Tags</th> +</tr> +{% for log in logs %} +<tr> + <td>{{ log.N }}</td> + <td>{{ log.created }}</td> + <td>{{ log.issuer }}</td> + <td>{{ log.comment }} + {% if log.filename %} + {{ log.filename }} + {% endif %} + </td> + <td> + {% for tag in log.tags.all %} + <div style="padding: 2px; display: inline; background-color: #000000; color: #FFFFFF;">{{ tag.displayName }}</div> + {% endfor %} + </td> +</tr> +{% endfor %} +</table> +</div> + +</div> <!-- end event_detail_content div --> +{% endblock %} + diff --git a/gracedb/superevents/views.py b/gracedb/superevents/views.py new file mode 100644 index 000000000..9711d946c --- /dev/null +++ b/gracedb/superevents/views.py @@ -0,0 +1,133 @@ +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse +from django.utils.html import escape +from django.views.decorators.http import require_POST + +from .models import Superevent, Log +from .forms import LogCreateForm + +from core.vfile import VersionedFile +from events.permission_utils import internal_user_required, is_external + +import os +import logging +logger = logging.getLogger(__name__) + + +# Need to restrict ability to view +def webview(request, superevent_id): + + # Get superevent object + superevent = Superevent.objects.get(id=superevent_id[1:]) + + # Get context + context = {} + context['superevent'] = superevent + context['preferred_event'] = superevent.preferred_event + + # Display far + display_far = superevent.preferred_event.far + far_is_upper_limit = False + if display_far and is_external(request.user): + if display_far < settings.VOEVENT_FAR_FLOOR: + display_far = settings.VOEVENT_FAR_FLOOR + far_is_upper_limit = True + context['display_far'] = display_far + context['far_is_upper_limit'] = far_is_upper_limit + display_far_yr = display_far + if display_far: + far_yr = display_far * (86400*365.25) # yr^-1 + if (far_yr < 1): + display_far_yr = "1 per {0:0.5g} years".format(1.0/far_yr) + else: + display_far_yr = "{0:0.5g} per year".format(far_yr) + context['display_far_yr'] = display_far_yr + + # Is the user an external user? (I.e., not part of the LVC?) The template + # needs to know that in order to decide what pieces of information to show. + context['user_is_external'] = is_external(request.user) + + # Pass event graceids + context['internal_events'] = superevent.get_internal_events().order_by('id') + context['external_events'] = superevent.get_external_events().order_by('id') + + # Form for log creation + context['log_create_form'] = LogCreateForm(initial={ + 'superevent': superevent.id}) + + # Temporary method for getting logs + context['logs'] = superevent.log_set.select_related('issuer').all() + + return render(request, 'superevent.html', context=context) + +# Need to add an auth check for this too +# If we use javascript for this eventually, we will want to enforce +# request.is_ajax() +@require_POST +def web_create_log(request, superevent_id): + """Webpage-based superevent log message creation""" + + # Set up dict for passing to log creation form ---------------------------- + log_dict = request.POST.copy() + log_dict['issuer'] = request.user.id + + # Get superevent id from superevent_id + # TODO: TEMPORARY until superevent_id is well defined + superevent = Superevent.objects.get(id=int(superevent_id[1:])) + log_dict['superevent'] = superevent.id + + # TODO: + # After getting superevent, make sure user has appropriate permissions + # to operate on it + + # File version stuff + data_file = request.FILES.get('data_file', None) if request.FILES else None + filename = getattr(data_file, 'name', None) + log_dict['filename'] = filename + log_dict['file_version'] = None # will be updated later, if applicable + log_dict['data_file'] = data_file + + # Validate with form and create new log object ---------------------------- + form = LogCreateForm(log_dict, request.FILES) + + # If form is valid, create new log object from form data + if form.is_valid(): + # Create new log object + obj = form.save() + + # Save data_file, if applicable + #if data_file: + + # # TODO: fix this! + # filepath = '/home/gracedb/' + filename + # #filepath = os.path.join(event.datadir, filename) + + # fdest = VersionedFile(filepath, 'w') + # for chunk in data_file.chunks(): + # fdest.write(chunk) + # fdest.close() + + # # Ascertain the version assigned to this particular file and update + # # the log object + # obj.file_version = fdest.version + # obj.save() + + # TODO: + # Attach "analyst_comments" tag - web view only + + # TODO: + # Send alert + + # TODO: + # attach external tagname if user is external + + # TODO: + # Don't have a good way to handle errors in the form at present - since we + # just redirect, we can't update the form with errors. We can just call + # the webview function with extra context, but then the URL is "wrong" + + + # Return to superevent page + return HttpResponseRedirect(reverse('superevents:view', + args=[superevent_id])) -- GitLab