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 %} &lt; {% endif %}{{ display_far|scientific }}</td>
+        <td>{% if far_is_upper_limit %} &lt; {% 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