From 88ed4890fd90f2f1d00ae0fabe7907700a90e9e9 Mon Sep 17 00:00:00 2001 From: Tanner Prestegard <tanner.prestegard@ligo.org> Date: Wed, 5 Sep 2018 12:33:07 -0500 Subject: [PATCH] Forms on superevent web display use AJAX to API Changing signoff form and expose/hide button on the superevent web display to make an AJAX request to the relevant API endpoints. This way, we don't have to maintain separate permissions and views for web resources. --- gracedb/events/models.py | 8 ++ gracedb/superevents/forms.py | 30 +----- gracedb/superevents/mixins.py | 99 +++++++++---------- gracedb/superevents/views.py | 42 +++----- gracedb/templates/superevents/detail.html | 69 +++++++------ .../superevents/superevent_detail_script.js | 85 ++++++++++++++++ 6 files changed, 195 insertions(+), 138 deletions(-) diff --git a/gracedb/events/models.py b/gracedb/events/models.py index 92770429a..a769a07eb 100644 --- a/gracedb/events/models.py +++ b/gracedb/events/models.py @@ -933,6 +933,14 @@ class SignoffBase(models.Model): signoff_type = models.CharField(max_length=3, blank=False, choices=SIGNOFF_TYPE_CHOICES) + # Timezones for instruments (this should really be handled separately + # by an instrument class) + instrument_time_zones = { + INSTRUMENT_H1: 'America/Los_Angeles', + INSTRUMENT_L1: 'America/Chicago', + INSTRUMENT_V1: 'Europe/Rome', + } + class Meta: abstract = True diff --git a/gracedb/superevents/forms.py b/gracedb/superevents/forms.py index af10d2052..e32c01a2c 100644 --- a/gracedb/superevents/forms.py +++ b/gracedb/superevents/forms.py @@ -2,8 +2,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from .models import Superevent, Log, Signoff -from .utils import create_log, create_signoff_for_superevent, \ - update_signoff_for_superevent +from .utils import create_log, create_signoff, update_signoff from core.forms import ModelFormUpdateMixin from core.vfile import VersionedFile @@ -14,42 +13,17 @@ logger = logging.getLogger(__name__) class SignoffForm(forms.ModelForm): - ACTION_CHOICES = ( - ('CR', 'create'), - ('UP', 'update'), - ) - action = forms.fields.ChoiceField(choices=ACTION_CHOICES) - delete = forms.fields.BooleanField(required=False) class Meta: model = Signoff - fields = ['status', 'comment', 'signoff_type', 'superevent', - 'submitter', 'instrument', 'delete'] + fields = ['status', 'comment', 'signoff_type', 'instrument'] def __init__(self, *args, **kwargs): super(SignoffForm, self).__init__(*args, **kwargs) # Hide some fields that we will populate either by default # when we instantiate the form or with the request data self.fields['signoff_type'].widget = forms.HiddenInput() - self.fields['superevent'].widget = forms.HiddenInput() - self.fields['submitter'].widget = forms.HiddenInput() self.fields['instrument'].widget = forms.HiddenInput() - self.fields['action'].widget = forms.HiddenInput() - - def save(self, *args, **kwargs): - if self.cleaned_data['action'] == 'CR': - signoff = create_signoff_for_superevent(self.instance.superevent, - self.instance.submitter, self.instance.signoff_type, - self.instance.instrument, self.instance.status, - self.instance.comment, add_log_message=True, issue_alert=True) - elif self.cleaned_data['action'] == 'UP': - signoff = update_signoff_for_superevent(self.instance, - self.instance.submitter, self.changed_data, - add_log_message=True, issue_alert=True) - else: - raise Exception('action must be CR (create) or UP (update)') - - return signoff class LogCreateForm(forms.ModelForm): diff --git a/gracedb/superevents/mixins.py b/gracedb/superevents/mixins.py index 639222896..b543e7bc2 100644 --- a/gracedb/superevents/mixins.py +++ b/gracedb/superevents/mixins.py @@ -1,13 +1,18 @@ # mixins for class-based views +import pytz + from django import forms from django.conf import settings from django.contrib.auth.models import Group as AuthGroup from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.views.generic.base import ContextMixin + from guardian.models import GroupObjectPermission +from core.time_utils import gpsToUtc from .forms import SignoffForm +from .models import Signoff import logging logger = logging.getLogger(__name__) @@ -34,7 +39,7 @@ class OperatorSignoffMixin(ContextMixin): # Determine if a signoff object already exists signoff = self.object.signoff_set.filter(instrument=signoff_instrument, - signoff_type='OP').first() + signoff_type=Signoff.SIGNOFF_TYPE_OPERATOR).first() # Check if label requesting signoff exists signoff_request_label_name = signoff_instrument + 'OPS' @@ -47,20 +52,27 @@ class OperatorSignoffMixin(ContextMixin): if not signoff_active: return context + # Get object time in operator timezone + obj_time_for_operator = gpsToUtc(self.object.gpstime).astimezone( + pytz.timezone(Signoff.instrument_time_zones[signoff_instrument])) + # Add more to context + context['object_gpstime_in_operator_tz'] = \ + obj_time_for_operator.strftime(settings.GRACE_STRFTIME_FORMAT) context['operator_signoff_instrument'] = signoff_instrument + context['operator_signoff_type'] = Signoff.SIGNOFF_TYPE_OPERATOR if signoff: # Populate form with instance - form = SignoffForm(initial={'action': 'UP'}, instance=signoff) + form = SignoffForm(instance=signoff) context['operator_signoff_exists'] = True else: # Default create form - form = SignoffForm(initial={'signoff_type': 'OP', - 'instrument': signoff_instrument, 'action': 'CR'}) + form = SignoffForm(initial={ + 'signoff_type': Signoff.SIGNOFF_TYPE_OPERATOR, + 'instrument': signoff_instrument, + }) context['operator_signoff_exists'] = False - # Hide delete checkbox - doesn't apply to creation - form.fields['delete'].widget=forms.HiddenInput() context['operator_signoff_form'] = form return context @@ -87,7 +99,7 @@ class AdvocateSignoffMixin(ContextMixin): # Determine if a signoff object already exists signoff = self.object.signoff_set.filter(instrument=signoff_instrument, - signoff_type='ADV').first() + signoff_type=Signoff.SIGNOFF_TYPE_ADVOCATE).first() # Check if label requesting signoff exists signoff_request_label_name = 'ADVREQ' @@ -102,66 +114,53 @@ class AdvocateSignoffMixin(ContextMixin): # Add more to context context['advocate_signoff_instrument'] = signoff_instrument + context['advocate_signoff_type'] = Signoff.SIGNOFF_TYPE_ADVOCATE if signoff: # Populate form with instance - form = SignoffForm(initial={'action': 'UP'}, instance=signoff) + form = SignoffForm(instance=signoff) context['advocate_signoff_exists'] = True else: # Default create form - form = SignoffForm(initial={'signoff_type': 'ADV', - 'instrument': signoff_instrument, 'action': 'CR'}) + form = SignoffForm(initial={ + 'signoff_type': Signoff.SIGNOFF_TYPE_ADVOCATE, + 'instrument': signoff_instrument, + }) context['advocate_signoff_exists'] = False - # Hide delete checkbox - doesn't apply to creation - form.fields['delete'].widget=forms.HiddenInput() context['advocate_signoff_form'] = form return context -class LvemPermissionMixin(ContextMixin): +class ExposeHideMixin(ContextMixin): + expose_perm_name = 'superevents.expose_superevent' + hide_perm_name = 'superevents.hide_superevent' + form_url_view_name = 'shib:default:superevents:superevent-permissions' def get_context_data(self, **kwargs): # Get base context - context = super(LvemPermissionMixin, self).get_context_data(**kwargs) - - # Get LV-EM observers group - lvem_obs_group = AuthGroup.objects.get( - name=settings.LVEM_OBSERVERS_GROUP) - - # Get permission objects - model_name = self.model.__name__.lower() - ctype = ContentType.objects.get(app_label=self.model._meta.app_label, - model=model_name) - p_view = Permission.objects.get(codename='view_{0}'.format(model_name)) - p_change = Permission.objects.get(codename='change_{0}'.format( - model_name)) - - # Determine - lvem_obs_can_view = GroupObjectPermission.objects.filter( - content_type=ctype, object_pk=self.object.pk, group=lvem_obs_group, - permission=p_view).exists() - lvem_obs_can_change = GroupObjectPermission.objects.filter( - content_type=ctype, object_pk=self.object.pk, group=lvem_obs_group, - permission=p_change).exists() - - # Determine user permissions for exposing to or protecting from - # the LV-EM observers group - if (lvem_obs_can_view and lvem_obs_can_change and - self.request.user.has_perm( - 'guardian.delete_groupobjectpermission')): - perms = False, True - elif (not lvem_obs_can_view and not lvem_obs_can_change and - self.request.user.has_perm( - 'guardian.add_groupobjectpermission')): - perms = True, False - else: - perms = False, False + context = super(ExposeHideMixin, self).get_context_data(**kwargs) + + # Determine if user can modify permissions to expose or hide + can_modify_permissions = False + if (self.request.user.has_perm(self.expose_perm_name) and + not self.object.is_exposed): + # Object is hidden and user can expose + can_modify_permissions = True + button_text = 'Make this superevent publicly visible' + action = 'expose' + elif (self.request.user.has_perm(self.hide_perm_name) and + self.object.is_exposed): + # Object is visible and user can hide + can_modify_permissions = True + button_text = 'Make this superevent internal-only' + action = 'hide' # Update context - context['can_expose_to_lvem'] = perms[0] - context['can_protect_from_lvem'] = perms[1] - context['lvem_group_name'] = settings.LVEM_OBSERVERS_GROUP + context['can_modify_permissions'] = can_modify_permissions + if can_modify_permissions: + context['permissions_form_button_text'] = button_text + context['permissions_action'] = action return context diff --git a/gracedb/superevents/views.py b/gracedb/superevents/views.py index 2efbc6070..607b88cf6 100644 --- a/gracedb/superevents/views.py +++ b/gracedb/superevents/views.py @@ -8,15 +8,19 @@ from django.views.generic.detail import DetailView from django.contrib.auth.models import Group as AuthGroup, Permission from django.contrib.contenttypes.models import ContentType from django.contrib import messages + from guardian.models import GroupObjectPermission +from guardian.shortcuts import assign_perm, remove_perm from .forms import LogCreateForm, SignoffForm -from .mixins import LvemPermissionMixin, OperatorSignoffMixin, \ +from .mixins import ExposeHideMixin, OperatorSignoffMixin, \ AdvocateSignoffMixin from .models import Superevent, Log from .utils import get_superevent_by_date_id_or_404, \ - confirm_superevent_as_gw, delete_signoff_for_superevent + confirm_superevent_as_gw, delete_signoff +from core.permission_utils import expose_event_or_superevent_to_lvem, \ + expose_event_or_superevent_to_public from core.http import check_and_serve_file from core.vfile import VersionedFile from events.models import EMGroup @@ -29,7 +33,7 @@ logger = logging.getLogger(__name__) class SupereventDetailView(OperatorSignoffMixin, AdvocateSignoffMixin, - LvemPermissionMixin, DetailView, DisplayFarMixin): + ExposeHideMixin, DetailView, DisplayFarMixin): model = Superevent template_name = 'superevents/detail.html' @@ -310,12 +314,6 @@ def modify_permissions(request, superevent_id): except AuthGroup.DoesNotExist: return HttpResponseNotFound('Group not found') - # Get content type and permissions - ctype = ContentType.objects.get(app_label='superevents', - model='superevent') - p_view = Permission.objects.get(codename='view_superevent') - p_change = Permission.objects.get(codename='change_superevent') - # Make sure the user is authorized. if action == 'expose': # Check permissions @@ -323,11 +321,8 @@ def modify_permissions(request, superevent_id): msg = "You aren't authorized to create permission objects." return HttpResponseForbidden(msg) - # Create GOPs - GroupObjectPermission.objects.get_or_create(content_type=ctype, - group=group, permission=p_view, object_pk=superevent.id) - GroupObjectPermission.objects.get_or_create(content_type=ctype, - group=group, permission=p_change, object_pk=superevent.id) + assign_perm('superevents.view_superevent', group, superevent) + assign_perm('superevents.annotate_superevent', group, superevent) elif action == 'protect': # Check permissions @@ -336,21 +331,8 @@ def modify_permissions(request, superevent_id): return HttpResponseForbidden(msg) # Delete gops - try: - gop = GroupObjectPermission.objects.get(content_type=ctype, - group=group, permission=p_view, object_pk=superevent.id) - gop.delete() - except GroupObjectPermission.DoesNotExist: - # Couldn't find it. Take no action. - pass - try: - gop = GroupObjectPermission.objects.get(content_type=ctype, - group=group, permission=p_change, object_pk=superevent.id) - gop.delete() - except GroupObjectPermission.DoesNotExist: - # Couldn't find it. Take no action. - pass - + remove_perm('superevent.view_superevent', group, superevent) + remove_perm('superevent.annotate_superevent', group, superevent) else: msg = "Unknown action. Choices are 'expose' and 'protect'." return HttpResponseBadRequest(msg) @@ -408,7 +390,7 @@ def modify_signoff(request, superevent_id): # Check for delete parameter. If True, just delete the signoff. if delete: - delete_signoff_for_superevent(signoff, request.user, + delete_signoff(signoff, request.user, add_log_message=True, issue_alert=True) messages.info(request, "Signoff deleted.") return HttpResponseRedirect(original_url) diff --git a/gracedb/templates/superevents/detail.html b/gracedb/templates/superevents/detail.html index 1f8c3befe..28c2feaf4 100644 --- a/gracedb/templates/superevents/detail.html +++ b/gracedb/templates/superevents/detail.html @@ -59,22 +59,15 @@ </div> {% endif %} +{# not sure why we need this if statement, maybe can delete in the future #} {% if 'lvem_view' not in request.path %} -<!-- XXX This next bit is super hacky. --> -{% if can_expose_to_lvem %} -<div class="content-area"> -<form action="{% url "superevents:modify-permissions" superevent.superevent_id %}" method="POST"> - <input type="hidden" name="group_name" value="{{ lvem_group_name }}"> - <input type="hidden" name="action" value="expose"> - <input type="submit" value="Expose this superevent to LV-EM" class="permButtonClass"> -</form> -</div> -{% elif can_protect_from_lvem %} + +{#-- XXX This next bit is super hacky. #} +{% if can_modify_permissions %} <div class="content-area"> -<form action="{% url "superevents:modify-permissions" superevent.superevent_id %}" method="POST"> - <input type="hidden" name="group_name" value="{{ lvem_group_name }}"> - <input type="hidden" name="action" value="protect"> - <input type="submit" value="Revoke LV-EM permissions for this superevent" class="permButtonClass"> +<form action="{% url "shib:default:superevents:superevent-permission-modify" superevent.superevent_id %}" method="POST" id="permissions_form"> + <input type="hidden" name="action" value="{{ permissions_action }}"> + <input type="submit" value="{{ permissions_form_button_text }}" class="permButtonClass"> </form> </div> {% endif %} @@ -86,28 +79,31 @@ {% if operator_signoff_exists %} <p>This event has already been signed off on. Use the form below if you wish to edit or delete the record.</p> {% else %} - <p>This superevent still requires operator signoff. Please answer the following (and optionally enter a comment): At the time of the - {% if operator_signoff_instrument == 'H1' %} - superevent ({{ superevent.t_0|gpsdate_tz:"lho" }}), - {% elif operator_signoff_instrument == 'L1' %} - superevent ({{ superevent.t_0|gpsdate_tz:"llo" }}), - {% elif operator_signoff_instrument == 'V1' %} - superevent ({{ superevent.t_0|gpsdate_tz:"virgo" }}), - {% else %} - superevent, - {% endif %} - was the operating status of the detector basically okay, or not?</p> + <p>This superevent still requires operator signoff. Please answer the following (and optionally enter a comment): At the time of the superevent ({{ object_gpstime_in_operator_tz }}), was the operating status of the detector basically okay, or not?</p> {% endif %} - <form action="{% url "superevents:modify-signoff" superevent.superevent_id %}" method="POST"> + <form class="signoff_form"> <table> {{ operator_signoff_form.as_table }} - <tr><td></td><td><input type="submit" value="Submit" class="searchButtonClass"></td></tr> + <tr> + <td></td> + <td> + {# inputs are disabled here, enabled by jquery code on page load. Otherwise users who click quickly can activate the form before the jquery is fully loaded #} + {% if operator_signoff_exists %} + {% with operator_signoff_type|add:operator_signoff_instrument as typeinst %} + <input type="submit" formaction="{% url "shib:default:superevents:superevent-signoff-detail" superevent.superevent_id typeinst %}" value="Update signoff" class="searchButtonClass" id="update" disabled> + <input type="submit" formaction="{% url "shib:default:superevents:superevent-signoff-detail" superevent.superevent_id typeinst %}" value="Delete signoff" class="searchButtonClass" id="delete" disabled> + {% endwith %} + {% else %} + <input type="submit" value="Create signoff" class="searchButtonClass" formaction={% url "shib:default:superevents:superevent-signoff-list" superevent.superevent_id %} disabled> + {% endif %} + </td> + </tr> </table> </form> </div> {% endif %} -<!-- Here is a section for the EM advocate signoffs. --> +{# Here is a section for the EM advocate signoffs. #} {% if advocate_signoff_authorized and advocate_signoff_active %} <div class="signoff-area"> <h2>Advocate Signoff</h2> @@ -119,10 +115,23 @@ {% endif %} </p> - <form action="{% url "superevents:modify-signoff" superevent.superevent_id %}" method="POST"> + <form class="signoff_form"> <table> {{ advocate_signoff_form.as_table }} - <tr><td></td><td><input type="submit" value="Submit" class="searchButtonClass"></td></tr> + <tr> + <td></td> + <td> + {# inputs are disabled here, enabled by jquery code on page load. Otherwise users who click quickly can activate the form before the jquery is fully loaded #} + {% if advocate_signoff_exists %} + {% with advocate_signoff_type|add:advocate_signoff_instrument as typeinst %} + <input type="submit" formaction="{% url "shib:default:superevents:superevent-signoff-detail" superevent.superevent_id typeinst %}" value="Update signoff" class="searchButtonClass" id="update" disabled> + <input type="submit" formaction="{% url "shib:default:superevents:superevent-signoff-detail" superevent.superevent_id typeinst %}" value="Delete signoff" class="searchButtonClass" id="delete" disabled> + {% endwith %} + {% else %} + <input type="submit" value="Create signoff" class="searchButtonClass" formaction={% url "shib:default:superevents:superevent-signoff-list" superevent.superevent_id %} disabled> + {% endif %} + </td> + </tr> </table> </form> </div> diff --git a/gracedb/templates/superevents/superevent_detail_script.js b/gracedb/templates/superevents/superevent_detail_script.js index 89320153f..4b4feae0d 100644 --- a/gracedb/templates/superevents/superevent_detail_script.js +++ b/gracedb/templates/superevents/superevent_detail_script.js @@ -224,6 +224,72 @@ require([ Save, Preview, ScrollPane, Uploader) { parser.parse(); + + + // We don't enable the input buttons until right now otherwise fast users + // can trigger the form before the javascript is ready... not ideal + $(".signoff_form input[type=submit]").attr('disabled', false); + + // Signoff form - determine HTTP method type and url based on + // attributes of button which was pressed. Then submit to + // API url and reload + $(".signoff_form").submit(function(e) { + e.preventDefault(); + + // Get HTTP method from button used to submit form + var submit_button = $(this).children("input[type=submit][clicked=true]"); + var button_id = submit_button.attr("id"); + var http_method = 'POST'; + if (button_id == 'update') { + http_method = 'PATCH'; + } else if (button_id == 'delete') { + http_method = 'DELETE'; + } + + // Get URL from button used to submit form; otherwise use + // main form action + var url = submit_button.attr('formaction'); + if (url === undefined) { + url = $(this).attr('action'); + } + + // Get and disable all submit buttons on the form + var all_submit_buttons = $(this).children('input[type=submit]'); + all_submit_buttons.attr("disabled", true); + + // Make ajax request + $.ajax({ + type: http_method, + url: url, + data: $(this).serialize(), + success: function(resp) { + // Don't need to re-enable since we reload the page + //all_submit_buttons.attr("disabled", false); + location.reload(true); + }, + error: function(error) { + //this.button.set("disabled", false); + var err_msg = "Error " + error.status + ": "; + if (error.responseText != "") { + err_msg += error.responseText; + } else { + err_msg += error.statusText; + } + if (error.status == 404) { + err_msg += ". Reload the page."; + } + alert(err_msg); + // Re-enable all submit buttons + all_submit_buttons.attr("disabled", false); + } + }); + }); + // Handles determination of which button was used to submit form + $(".signoff_form input[type=submit]").click(function() { + $("input[type=submit]", $(this).parents("form")).removeAttr("clicked"); + $(this).attr("clicked", true); + }); + //---------------------------------------------------------------------------------------- // Some utility functions //---------------------------------------------------------------------------------------- @@ -1169,6 +1235,25 @@ require([ var i3 = put(f, 'input[id="hidden_tagname"][name="tagname"][type="hidden"]'); put(upload_div, 'br'); + // Permissions form - submit to URL and reload + $("#permissions_form").submit(function(e) { + e.preventDefault(); + $.ajax({ + type: 'POST', + url: $(this).attr('action'), + data: $(this).serialize(), + success: function(resp) { + //this.button.set("disabled", false); + location.reload(true); + }, + error: function(error) { + //this.button.set("disabled", false); + alert(error); + } + }); + }); + + $("#emo_submit_form").submit(function(e) { e.preventDefault(); $.ajax({ -- GitLab