diff --git a/gracedb/events/models.py b/gracedb/events/models.py index 92770429a4953867d6136555e2c6fdceaac4d900..a769a07eb3272fe17f0bd53230fa58dc53ba1a07 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 af10d2052bee686a0aedf7ce772415a85990a0ac..e32c01a2c95f95c650773905feaacbb86cc64568 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 639222896e34c65e8475bf2dec7e2586638742db..b543e7bc22bc339ff61afd43a5e9e8e034c84e62 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 2efbc6070b31138dfc6d90a2e44201f3dd766119..607b88cf6ec64bd16dae43cebb23d6beb856bd7b 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 1f8c3befe2cdf51f4a0d9d0173a8f0a2d28876e8..28c2feaf43fda6dd1316ce4dfe264323d8bb6d50 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 89320153f7d10d6f78738484dcb6c4b031aa5467..4b4feae0d436021b72777ace0e9c9b7ee5b62776 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({