diff --git a/config/settings/base.py b/config/settings/base.py index 074c1eab13383bfcd1b5ad15c7ca1356a4675118..070ecc8187223dabe9a55067fb812a28c92aabb5 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -687,3 +687,7 @@ PUBLIC_PAGE_RUNS = ['O4', 'ER15', 'O3'] # Define how long to cache the public page: PUBLIC_PAGE_CACHING = int(get_from_env('DJANGO_PUBLIC_PAGE_CACHING', fail_if_not_found=False, default_value=300)) + +# Define the number of results per page on the public page: +PUBLIC_PAGE_RESULTS = int(get_from_env('DJANGO_PUBLIC_PAGE_RESULTS', + fail_if_not_found=False, default_value=15)) diff --git a/gracedb/superevents/templatetags/public_alerts.py b/gracedb/superevents/templatetags/public_alerts.py index 9a35faccf0c21b2a4eac427f147628559b44ad18..7bb986da6b2371a47ee8085dd842626530eb34ab 100644 --- a/gracedb/superevents/templatetags/public_alerts.py +++ b/gracedb/superevents/templatetags/public_alerts.py @@ -24,7 +24,7 @@ def omegascan_links(superevent, run): return mark_safe(o3_deepomegascan_fmt.format(sid=superevent.superevent_id)) else: link_list = [] - for filename in superevent.log_set.filter(filename__contains='omegascan').order_by('filename').values_list('filename', flat=True): + for filename in sorted(set(superevent.log_set.filter(filename__contains='omegascan').order_by('filename').values_list('filename', flat=True))): # get the absolute uri for the file: file_url = build_absolute_uri(reverse('api:default:superevents:superevent-file-detail', args=[superevent.superevent_id, filename])) diff --git a/gracedb/superevents/views.py b/gracedb/superevents/views.py index 4b17db12606e82b7a6d0069c9c08eb33ef02b811..90b9ca3d4bbf245a4269c552c2f1197a6900460b 100644 --- a/gracedb/superevents/views.py +++ b/gracedb/superevents/views.py @@ -4,11 +4,13 @@ from lal import gpstime from django.conf import settings from django.db.models import Q -from django.http import Http404 -from django.shortcuts import redirect +from django.http import Http404, JsonResponse +from django.shortcuts import redirect, render +from django.template.loader import render_to_string from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers from django.views.generic.detail import DetailView from django.views.generic import ListView @@ -187,10 +189,13 @@ def public_alerts_redirect(request): run=settings.PUBLIC_PAGE_RUNS[0])) # The public alerts page: +#@method_decorator(cache_page(0), name='dispatch') @method_decorator(cache_page(settings.PUBLIC_PAGE_CACHING), name='dispatch') +@method_decorator(vary_on_headers('X-Requested-With'), name='dispatch') @method_decorator(public_if_public_access_allowed, name='dispatch') class SupereventPublic(DisplayFarMixin, ListView): model = Superevent + paginate_by = settings.PUBLIC_PAGE_RESULTS template_name = 'superevents/public_alerts.html' filter_permissions = ['superevents.view_superevent'] log_view_permission = 'superevents.view_log' @@ -200,8 +205,72 @@ class SupereventPublic(DisplayFarMixin, ListView): default_skymap_filename = 'bayestar.png' burst_skymap_filename = '{pipeline}.png' pe_results_tagname = 'pe_results' + show_button_text = 'Show All Public Events' + hide_button_text = 'Show Significant Events Only' + + button_text_dict = {'show_button_text': show_button_text, + 'hide_button_text': hide_button_text + } + + # Override the standard get() method to allow for browser vs ajax requests. + def get(self, request, **args): + # For ajax requests, optionally get the "showall" value to to reveal + # insignificant events. Otherwise, for browser requests (the default when + # a user first lands on the public page), then only show significant. + allow_empty = self.get_allow_empty() + + if not allow_empty: + # When pagination is enabled and object_list is a queryset, + # it's better to do a cheap query than to load the unpaginated + # queryset in memory. + if self.get_paginate_by(self.object_list) is not None and hasattr( + self.object_list, "exists" + ): + is_empty = not self.object_list.exists() + else: + is_empty = not self.object_list + if is_empty: + raise Http404( + _("Empty list and “%(class_name)s.allow_empty†is False.") + % { + "class_name": self.__class__.__name__, + } + ) + + showall = request.GET.get('showall', None) + if showall and showall.lower() in ['true', 't', '1', 'y']: + showall=True + button_text = self.hide_button_text + else: + showall=False + button_text = self.show_button_text + + if request.headers.get('x-requested-with') == 'XMLHttpRequest': + self.object_list = self.get_queryset(showall) + context = self.get_context_data() + context['showall'] = int(showall) + context['button_text'] = button_text + rendered_table = render_to_string('superevents/public_alerts_table.html', + context=context, request=request) + data = {'rendered_table': rendered_table} + return JsonResponse(data) + else: + self.object_list = self.get_queryset(showall) + context = self.get_context_data() + context['showall'] = int(showall) + context['button_text'] = button_text + return render(request, self.template_name, context=context) + - def get_queryset(self, **kwargs): + # Modify get_queryset() to filter for significant vs insignificant: + def get_queryset(self, showall=False, **kwargs): + if showall: + return self.get_public_superevents() + else: + return self.significant_events(self.get_public_superevents()) + + # Get all publicly exposed production superevents: + def get_public_superevents(self, **kwargs): # Query only for public events for the given observation run. # if it's not in the run list, return a 404. self.obsrun = self.kwargs.get('obsrun') @@ -214,7 +283,8 @@ class SupereventPublic(DisplayFarMixin, ListView): .prefetch_related('voevent_set', 'log_set') return qs - # Define insignificance per run: + + # Define significance per run: def significant_events(self, sevents): # We're checking for ADVREQ|ADVOK|ADVNO # https://git.ligo.org/computing/gracedb/server/-/issues/303#note_725082 @@ -284,14 +354,16 @@ class SupereventPublic(DisplayFarMixin, ListView): def get_context_data(self, **kwargs): context = super(SupereventPublic, self).get_context_data(**kwargs) + # Get the total number of public events: + context['total_events'] = self.get_public_superevents().count() + # For each superevent, get list of log messages and construct pastro # string - table_data = {} candidates = 0 retractions = 0 # get insignificant events - sig_events = self.significant_events(self.object_list) + sig_events = self.significant_events(self.get_public_superevents()) # Filter and loop over exposed superevents for the given run: for se in self.object_list: @@ -369,22 +441,40 @@ class SupereventPublic(DisplayFarMixin, ListView): se.sourcetypes = ', '.join(sourcelist) - # Now add it to the output dict, with the key being the run value: - table_data['events'] = self.object_list - - total_events = self.object_list.count() - # export the dictionary for rendering: context['signif_docs'] = self.insignificant_docs(self.obsrun) - context['data'] = table_data + context['events'] = self.object_list context['run'] = self.obsrun - context['total_events'] = self.object_list.count() context['total_sig'] = sig_events.count() context['total_insig'] = context['total_events'] - context['total_sig'] context['candidates'] = candidates context['retractions'] = retractions context['sig_cands'] = context['total_sig'] - retractions + # export the button text: + context.update(self.button_text_dict) + + # update pagination context: + page_size = self.get_paginate_by(self.object_list) + context_object_name = self.get_context_object_name(self.object_list) + if page_size: + paginator, page, self.object_list, is_paginated = self.paginate_queryset( + self.object_list, page_size + ) + context.update({ + "paginator": paginator, + "page_obj": page, + "is_paginated": is_paginated, + "object_list": self.object_list, + }) + else: + context.update({ + "paginator": None, + "page_obj": None, + "is_paginated": False, + "object_list": queryset, + }) + return context # FIXME: this is vestigial code from a planned curated view. I'm going diff --git a/gracedb/templates/superevents/public_alerts.html b/gracedb/templates/superevents/public_alerts.html index 39b7f71fde5f0136f70db8a203a112e25f934952..1a5d7e1833c93b4b25ea67ecb41f66c544b2d9db 100644 --- a/gracedb/templates/superevents/public_alerts.html +++ b/gracedb/templates/superevents/public_alerts.html @@ -1,8 +1,4 @@ {% extends "base.html" %} -{% load cache %} -{% cache 300 publicalerts %} -{% load sanitize_html %} -{% load logtags %} {% load static %} {% load public_alerts %} @@ -18,9 +14,9 @@ {% block pageid %}public{% endblock %} {% block jscript %} -<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tablesaw/3.1.2/tablesaw.css" integrity="sha512-Gh62F+VzywHWpYtkkUqg2QmF2B67AeDmZv6uXznRUxXNUCFrRFlZRI2gKo2Bf3cgG5dAxAgxRJah3GgpJ0Z1Mw==" crossorigin="anonymous" referrerpolicy="no-referrer" /> -<script src="https://cdnjs.cloudflare.com/ajax/libs/tablesaw/3.1.2/tablesaw.js" integrity="sha512-pJ/SPF4B6ZzEZp4b86/xpcZdcVkqtwyd/orBJQwOasT9S0qyIHcis2wRS/WGV4T4J8YrwqA/rPm+jDwQKkQs4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> -<script src="https://cdnjs.cloudflare.com/ajax/libs/tablesaw/3.1.2/tablesaw-init.js" integrity="sha512-ct5fXUTmBz4voTyyxShWMjCpNKnAkw3zWIKETeQi8NBa9lPG6/Gf4b4+XvjB1kgaXzwM3e5R2EXF8J7+WsYu/w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> +<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tablesaw/3.1.2/tablesaw.min.css" integrity="sha512-FW7xwIsGt3iCswNR3o4kcXqkKDvN8Q7obwdxFODRT+L6XkRtScLElfvobA7bjiC2iTgG/sRaBUqdrhqx0RTauw==" crossorigin="anonymous" referrerpolicy="no-referrer" /> +<script src="https://cdnjs.cloudflare.com/ajax/libs/tablesaw/3.1.2/tablesaw.jquery.min.js" integrity="sha512-/Un6gjn0IXHJtR9j8VBYmN3ZoAVNMTQTufizEkeImLkb67lxsJcXv/cFswbOig2v5ykbGxX6zngh4u8ff7FJ0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> +<script src="https://cdnjs.cloudflare.com/ajax/libs/tablesaw/3.1.2/tablesaw-init.min.js" integrity="sha512-w/1hLca9Fyn6iwnkMxUU55d/OlcA65RnP4v6kJ8NssnzHeHnJBeOzuHEkJ67zpt0ksiH1LQn8FKYZc/TT1O/aQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> {% endblock %} {% block content %} @@ -32,6 +28,7 @@ <li>Retractions are marked in <span style="color:red;">red</span>. Retraction means that the candidate was manually vetted and is no longer considered a candidate of interest.</li> {% if signif_docs %} <li>Less-significant events are marked in <span style="background-color: #E5E4E2">grey</span>, and are not manually vetted. Consult the <a href=" {{ signif_docs }}" target="_blank">LVK Alerts User Guide</a> for more information on significance in {{ run }}.</li> + <li>Less-significant events are not shown by default. Press "<b>Show All Public Events</b>" to show significant and less-significant events.</li> {% endif %} </ul> <hr> @@ -53,103 +50,63 @@ {% if signif_docs %} <script> $(document).ready(function(){ - $('#hideInsigButton').on('click', function () { - var text=$('#hideInsigButton').text(); - if(text === 'Show Significant Events Only'){ - $(this).html('Show All Public Events'); + $('#showAllButton').on('click', function () { + var text=$('#showAllButton').text(); + if(text === '{{ show_button_text }}'){ + $(this).html('{{ hide_button_text }}'); + $.ajax({ + url: '{{ request.path }}', + type: "get", + cache: true, + dataType: 'html', + data : { + showall: "True" + }, + success: function (data) { + var result = JSON.parse(data) + //alert(result.rendered_table) + $('#publicTableDiv').html(result.rendered_table); + $('#publicTable').trigger("enhance.tablesaw"); + }, + error: function (data) { + alert("Error rendering data"); + } + }); } else{ - $(this).html('Show Significant Events Only'); + $(this).html('{{ show_button_text }}'); + $.ajax({ + url: '{{ request.path }}', + type: "get", + cache: true, + dataType: 'html', + data : { + showall: "False" + }, + success: function (data) { + var result = JSON.parse(data) + //alert(result.rendered_table) + $('#publicTableDiv').html(result.rendered_table); + $('#publicTable').trigger("enhance.tablesaw"); + }, + error: function (data) { + alert("Error rendering data"); + } + }); } }); }); </script> - <a class="btn btn-primary" role="button" data-toggle="collapse" href="#insignificant" aria-expanded="true" aria-controls="insignificant" id="hideInsigButton">Show Significant Events Only</a> + <a class="btn btn-primary" role="button" href="#" id="showAllButton">{{ button_text }}</a> + <br> <br> {% endif %} {% if candidates > 0 or retractions > 0 %} - <div class="tablesaw-overflow"> - <table class="tablesaw table-condensed table-resp-gracedb shadow p-3 mb-5 rounded" data-tablesaw-sortable data-tablesaw-sortable-switch data-tablesaw-mode="columntoggle" data-tablesaw-minimap> - <thead class="table-detail-th"><tr> - <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="persist">Event ID</th> - <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="1">Possible Source (Probability)</th> - {% if signif_docs %} - <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="1">Significant</th> - {% endif %} - <th scope="col" data-tablesaw-sortable-col data-tablesaw-sortable-string data-tablesaw-priority="2">UTC</th> - <th scope="col" data-tablesaw-col data-tablesaw-priority="3">GCN</th> - <th scope="col" data-tablesaw-col data-tablesaw-priority="4">Location</th> - <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="5">FAR</th> - <th scope="col" data-tablesaw-col data-tablesaw-priority="6">Comments</th> - <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="0">GPS</th> - - {% if user_is_internal %} - <th scope="col" data-tablesaw-col data-tablesaw-priority="6">Ω Scan</th> - {% endif %} - - </tr> - </thead> - <tbody> - - - {% for event in data.events %} - - {% if event.retract %} - <tr style="background-color: #EDD;" {% if not event.signif %} class="collapse show" id="insignificant" {% endif %}> - {% elif not event.signif %} - <tr style="background-color: #E5E4E2" class="collapse show" id="insignificant"> - {% endif %} - - <td style="min-width: 100px;"> - <a href="{% url "superevents:view" event.superevent_id %}">{{ event.default_superevent_id }}</a> - </td> - <td style="min-width: 100px;">{{ event.sourcetypes }}</td> - {% if signif_docs %} - <td style="min-width: 100px;">{% if event.signif %} Yes {% else %} No {% endif %}</td> - {% endif %} - <td style="min-width: 140px;"> - <span style="font-size: 0px;">{{ event.t_0|floatformat:2 }}</span>{{ event.t_0_date }}<br/> - {{ event.t0_utc }} UTC - </td> - <td style="min-width: 110px;"> - {% if run == "O3" %} - <a href={{ event.gcnurl }}>GCN Circulars</a><br/> - {% else %} - <a href={{ event.gcnurl }}>GCN Circular Query</a><br/> - {% endif %} - - <a href={{ event.noticeurl }}>Notices</a> | <a href="{% url "legacy_apiweb:default:superevents:superevent-voevent-list" event.default_superevent_id %}">VOE</a> - </td> - <td> - {% if event.skymap_image %} - <a href="{{ event.skymap_image }}"> - <img src="{{ event.skymap_image }}" width="120px" /> - </a> - {% else %} - No public skymap image found. - {% endif %} - </td> - <td style="min-width: 120px;"> - <!-- Hidden float FAR value for table sorting --> - <span style="font-size: 0px;">{{ event.far_hz|floatformat:20 }}</span>{{ event.far_hr }} - </td> - <td style="min-width: 80px;">{{ event.comments|safe }} </td> - <td>{{ event.t_0|floatformat:2 }} </td> - - {% if user_is_internal %} - <!-- Add Omega scans and PE results for LVC users --> - <td style="min-width: 70px;"> - {{ event | omegascan_links:run }} - </td> - {% endif %} - - </tr> - {% endfor %} - </tbody></table> + <div id="publicTableDiv"> + {% include "superevents/public_alerts_table.html" %} </div> - {% else %} <br /> <h5 style='margin-left: 40px; font-weight: lighter;'>No {{ run }} candidates at present.</h5> @@ -162,4 +119,3 @@ </div> {% endblock %} -{% endcache %} diff --git a/gracedb/templates/superevents/public_alerts_table.html b/gracedb/templates/superevents/public_alerts_table.html new file mode 100644 index 0000000000000000000000000000000000000000..6c553c134c898ad9775c6b7e02262ae9e2e1eb0f --- /dev/null +++ b/gracedb/templates/superevents/public_alerts_table.html @@ -0,0 +1,98 @@ +{% load public_alerts %} + +<div class="pagination"> + <span class="step-links" style="font-weight: lighter;"> + {% if page_obj.has_previous %} + <a href="?page=1&showall={{ showall }}">« first</a> + <a href="?page={{ page_obj.previous_page_number }}&showall={{ showall }}">previous</a> + {% endif %} + + <span class="current"> + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}. + </span> + + {% if page_obj.has_next %} + <a href="?page={{ page_obj.next_page_number }}&showall={{ showall }}">next</a> + <a href="?page={{ page_obj.paginator.num_pages }}&showall={{ showall }}">last »</a> + {% endif %} + </span> +</div> +<div class="tablesaw-overflow"> + <table class="tablesaw table-condensed table-resp-gracedb shadow p-3 mb-5 rounded" data-tablesaw-sortable data-tablesaw-sortable-switch data-tablesaw-mode="columntoggle" data-tablesaw-minimap id="publicTable"> + <thead class="table-detail-th"><tr> + <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="persist">Event ID</th> + <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="1">Possible Source (Probability)</th> + {% if signif_docs %} + <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="1">Significant</th> + {% endif %} + <th scope="col" data-tablesaw-sortable-col data-tablesaw-sortable-string data-tablesaw-priority="2">UTC</th> + <th scope="col" data-tablesaw-col data-tablesaw-priority="3">GCN</th> + <th scope="col" data-tablesaw-col data-tablesaw-priority="4">Location</th> + <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="5">FAR</th> + <th scope="col" data-tablesaw-col data-tablesaw-priority="6">Comments</th> + <th scope="col" data-tablesaw-sortable-col data-tablesaw-priority="0">GPS</th> + + {% if user_is_internal %} + <th scope="col" data-tablesaw-col data-tablesaw-priority="6">Ω Scan</th> + {% endif %} + + </tr> + </thead> + <tbody> + + {% for event in page_obj %} + + {% if event.retract %} + <tr style="background-color: #EDD;" {% if not event.signif %} class="collapse show" id="insignificant" {% endif %}> + {% elif not event.signif %} + <tr style="background-color: #E5E4E2" class="collapse show" id="insignificant"> + {% endif %} + + <td style="min-width: 100px;"> + <a href="{% url "superevents:view" event.superevent_id %}">{{ event.default_superevent_id }}</a> + </td> + <td style="min-width: 100px;">{{ event.sourcetypes }}</td> + {% if signif_docs %} + <td style="min-width: 100px;">{% if event.signif %} Yes {% else %} No {% endif %}</td> + {% endif %} + <td style="min-width: 140px;"> + <span style="font-size: 0px;">{{ event.t_0|floatformat:2 }}</span>{{ event.t_0_date }}<br/> + {{ event.t0_utc }} UTC + </td> + <td style="min-width: 110px;"> + {% if run == "O3" %} + <a href={{ event.gcnurl }}>GCN Circulars</a><br/> + {% else %} + <a href={{ event.gcnurl }}>GCN Circular Query</a><br/> + {% endif %} + + <a href={{ event.noticeurl }}>Notices</a> | <a href="{% url "legacy_apiweb:default:superevents:superevent-voevent-list" event.default_superevent_id %}">VOE</a> + </td> + <td> + {% if event.skymap_image %} + <a href="{{ event.skymap_image }}"> + <img src="{{ event.skymap_image }}" width="120px" /> + </a> + {% else %} + No public skymap image found. + {% endif %} + </td> + <td style="min-width: 120px;"> + <!-- Hidden float FAR value for table sorting --> + <span style="font-size: 0px;">{{ event.far_hz|floatformat:20 }}</span>{{ event.far_hr }} + </td> + <td style="min-width: 80px;">{{ event.comments|safe }} </td> + <td>{{ event.t_0|floatformat:2 }} </td> + + {% if user_is_internal %} + <!-- Add Omega scans and PE results for LVC users --> + <td style="min-width: 70px;"> + {{ event | omegascan_links:run }} + </td> + {% endif %} + + </tr> + {% endfor %} + </tbody></table> +</div> +