diff --git a/gracedb/events/mixins.py b/gracedb/events/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..b5d0659daff34c3e58e8631035a0cada2a380ee2 --- /dev/null +++ b/gracedb/events/mixins.py @@ -0,0 +1,34 @@ +# mixins for class-based views + +from django.conf import settings + +from .permission_utils import is_external + +class DisplayFarMixin(object): + + def get_display_far(self, obj=None): + # obj should be an Event object + if obj is None: + obj = self.object + user = self.request.user + + # Determine FAR to display + display_far = obj.far + far_is_upper_limit = False + if (display_far and is_external(user) and + display_far < settings.VOEVENT_FAR_FLOOR): + + display_far = settings.VOEVENT_FAR_FLOOR + far_is_upper_limit = True + + # Determine "human-readable" FAR to display + display_far_hr = display_far + if display_far: + # FAR in units of yr^-1 + far_yr = display_far * (86400*365.25) + if (far_yr < 1): + display_far_hr = "1 per {0:0.5g} years".format(1.0/far_yr) + else: + display_far_hr = "{0:0.5g} per year".format(far_yr) + + return display_far, display_far_hr, far_is_upper_limit diff --git a/gracedb/superevents/forms.py b/gracedb/superevents/forms.py index f8895fe826849f404f6263eff76902af154496b9..af10d2052bee686a0aedf7ce772415a85990a0ac 100644 --- a/gracedb/superevents/forms.py +++ b/gracedb/superevents/forms.py @@ -1,11 +1,11 @@ from django import forms from django.utils.translation import ugettext_lazy as _ -from .models import Superevent, Log -from .utils import create_log +from .models import Superevent, Log, Signoff +from .utils import create_log, create_signoff_for_superevent, \ + update_signoff_for_superevent from core.forms import ModelFormUpdateMixin from core.vfile import VersionedFile -from events.models import Event import os @@ -13,6 +13,45 @@ import logging 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'] + + 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): # This field is used to get file upload, but is not actually # part of the Log model diff --git a/gracedb/superevents/mixins.py b/gracedb/superevents/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..639222896e34c65e8475bf2dec7e2586638742db --- /dev/null +++ b/gracedb/superevents/mixins.py @@ -0,0 +1,167 @@ +# mixins for class-based views +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 .forms import SignoffForm + +import logging +logger = logging.getLogger(__name__) + + +class OperatorSignoffMixin(ContextMixin): + + def get_context_data(self, **kwargs): + context = super(OperatorSignoffMixin, self).get_context_data(**kwargs) + + # Check if user is in auth group for which signoff is authorized + signoff_group = self.request.user.groups.filter( + name__icontains='control_room').first() + + # Update context with signoff_authorized bool + context['operator_signoff_authorized'] = signoff_group is not None + + # If not, just return + if not signoff_group: + return context + + # Get signoff instrument + signoff_instrument = signoff_group.name[:2].upper() + + # Determine if a signoff object already exists + signoff = self.object.signoff_set.filter(instrument=signoff_instrument, + signoff_type='OP').first() + + # Check if label requesting signoff exists + signoff_request_label_name = signoff_instrument + 'OPS' + signoff_request_label_exists = self.object.labelling_set.filter( + label__name=signoff_request_label_name).exists() + + # Should form object be shown to authorized users? + signoff_active = signoff_request_label_exists or signoff is not None + context['operator_signoff_active'] = signoff_active + if not signoff_active: + return context + + # Add more to context + context['operator_signoff_instrument'] = signoff_instrument + if signoff: + # Populate form with instance + form = SignoffForm(initial={'action': 'UP'}, instance=signoff) + context['operator_signoff_exists'] = True + else: + # Default create form + form = SignoffForm(initial={'signoff_type': 'OP', + 'instrument': signoff_instrument, 'action': 'CR'}) + 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 + + +class AdvocateSignoffMixin(ContextMixin): + + def get_context_data(self, **kwargs): + context = super(AdvocateSignoffMixin, self).get_context_data(**kwargs) + + # Check if user is in auth group for which signoff is authorized + signoff_group = self.request.user.groups.filter( + name=settings.EM_ADVOCATE_GROUP) + + # Update context with signoff_authorized bool + context['advocate_signoff_authorized'] = signoff_group is not None + + # If not, just return + if not signoff_group: + return context + + # Get signoff instrument + signoff_instrument = "" + + # Determine if a signoff object already exists + signoff = self.object.signoff_set.filter(instrument=signoff_instrument, + signoff_type='ADV').first() + + # Check if label requesting signoff exists + signoff_request_label_name = 'ADVREQ' + signoff_request_label_exists = self.object.labelling_set.filter( + label__name=signoff_request_label_name).exists() + + # Should form object be shown to authorized users? + signoff_active = signoff_request_label_exists or signoff is not None + context['advocate_signoff_active'] = signoff_active + if not signoff_active: + return context + + # Add more to context + context['advocate_signoff_instrument'] = signoff_instrument + if signoff: + # Populate form with instance + form = SignoffForm(initial={'action': 'UP'}, instance=signoff) + context['advocate_signoff_exists'] = True + else: + # Default create form + form = SignoffForm(initial={'signoff_type': 'ADV', + 'instrument': signoff_instrument, 'action': 'CR'}) + 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): + + 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 + + # Update context + context['can_expose_to_lvem'] = perms[0] + context['can_protect_from_lvem'] = perms[1] + context['lvem_group_name'] = settings.LVEM_OBSERVERS_GROUP + + return context diff --git a/gracedb/superevents/urls.py b/gracedb/superevents/urls.py index a504340b699327c25fa86d64cc59fa2c7ca6449e..1d1218eb21773309a86fcc00eb82544a70df3a27 100644 --- a/gracedb/superevents/urls.py +++ b/gracedb/superevents/urls.py @@ -1,29 +1,46 @@ -from django.conf.urls import url +from django.conf.urls import url, include from .models import Superevent from . import views app_name = 'superevents' -urlpatterns = [ - #url(r'^$', views.index, name="index"), - #url(r'^create/$', views.create, name="create"), - url(r'^(?P<superevent_id>{regex})/view/$'.format( - regex=Superevent.ID_REGEX), views.webview, name="view"), - url(r'^create_log/(?P<superevent_id>{regex})/$'.format( - regex=Superevent.ID_REGEX), views.web_create_log, name="create-log"), - url(r'^confirm_as_gw/(?P<superevent_id>{regex})/$'.format( - regex=Superevent.ID_REGEX), views.confirm_as_gw, name="confirm-gw"), +# URLs which are nested below a superevent detail +# These are included under a superevent's ID URL prefix (see below) +suburlpatterns = [ + + # Superevent detail view + url(r'^view/$', views.SupereventDetailView.as_view(), name="view"), + #url(r'^(?P<superevent_id>{regex})/oldview/$'.format( + # regex=Superevent.ID_REGEX), views.old_webview, name="oldview"), + #url(r'^(?P<superevent_id>{regex})/create_log/$'.format( + # regex=Superevent.ID_REGEX), views.web_create_log, name="create-log"), + + # Confirm as GW + url(r'^confirm_as_gw/$', views.confirm_as_gw, name="confirm-gw"), # Files - url(r'^(?P<superevent_id>{regex})/files/$'.format( - regex=Superevent.ID_REGEX), views.file_list, name="file-list"), - url(r'^(?P<superevent_id>{regex})/files/(?P<filename>.*)$'.format( - regex=Superevent.ID_REGEX), views.file_download, name="file-download"), + url(r'^files/$', views.file_list, name="file-list"), + url(r'^files/(?P<filename>.*)$', views.file_download, name="file-download"), + + # Changing LV-EM observers' superevent view/change permissions + url(r'^perms/$', views.modify_permissions, name="modify-permissions"), + # Signoff updates + url(r'^signoff/$', views.modify_signoff, name="modify-signoff"), +] + +# Legacy URL patterns +legacy_urlpatterns = [ # Legacy URLs for superevent detail view url(r'^(?P<superevent_id>{regex})/$'.format( - regex=Superevent.ID_REGEX), views.webview, name="legacyview1"), + regex=Superevent.ID_REGEX), views.SupereventDetailView.as_view(), name="legacyview1"), url(r'^view/(?P<superevent_id>{regex})/$'.format( - regex=Superevent.ID_REGEX), views.webview, name="legacyview2"), + regex=Superevent.ID_REGEX), views.SupereventDetailView.as_view(), name="legacyview2"), +] +# Full urlpatterns: legacy urls plus suburlpatterns nested under +# superevent_id +urlpatterns = legacy_urlpatterns + [ + url(r'^(?P<superevent_id>{regex})/'.format(regex=Superevent.ID_REGEX), + include(suburlpatterns)), ] diff --git a/gracedb/superevents/views.py b/gracedb/superevents/views.py index aaf0ec27a716aab36f12afafe39485d355b1367e..d9929cd6c1c9aca06fc6b60edff595ccab705243 100644 --- a/gracedb/superevents/views.py +++ b/gracedb/superevents/views.py @@ -4,13 +4,23 @@ from django.shortcuts import render from django.urls import reverse from django.utils.html import escape from django.views.decorators.http import require_POST, require_GET - +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 .forms import LogCreateForm, SignoffForm +from .mixins import LvemPermissionMixin, OperatorSignoffMixin, \ + AdvocateSignoffMixin from .models import Superevent, Log -from .forms import LogCreateForm -from .utils import get_superevent_by_date_id_or_404, confirm_superevent_as_gw +from .utils import get_superevent_by_date_id_or_404, \ + confirm_superevent_as_gw, delete_signoff_for_superevent from core.http import check_and_serve_file from core.vfile import VersionedFile +from events.models import EMGroup +from events.mixins import DisplayFarMixin from events.permission_utils import internal_user_required, is_external import os @@ -18,8 +28,79 @@ import logging logger = logging.getLogger(__name__) +class SupereventDetailView(OperatorSignoffMixin, AdvocateSignoffMixin, + LvemPermissionMixin, DetailView, DisplayFarMixin): + model = Superevent + template_name = 'superevents/detail.html' + + # TODO: + # May want to override this to select superevents by user + def get_queryset(self): + qs = super(SupereventDetailView, self).get_queryset() + + # Do some optimization + qs = qs.select_related('preferred_event__group', + 'preferred_event__pipeline', 'preferred_event__search') + qs = qs.prefetch_related('labelling_set', 'events') + + return qs + + def get_object(self, queryset=None): + if queryset is None: + queryset = self.get_queryset() + superevent_id = self.kwargs.get('superevent_id') + obj = get_superevent_by_date_id_or_404(self.request, superevent_id, + queryset) + return obj + + def get_context_data(self, **kwargs): + # Get base context + context = super(SupereventDetailView, self).get_context_data(**kwargs) + + # Add a bunch of extra stuff + superevent = self.object + context['preferred_event'] = superevent.preferred_event + context['preferred_event_labelling'] = superevent.preferred_event \ + .labelling_set.prefetch_related('label', 'creator').all() + + # TODO: can we optimize this more? + # TODO: also need to filter events for user + # Pass event graceids + context['internal_events'] = superevent.get_internal_events() \ + .order_by('id') + context['external_events'] = superevent.get_external_events() \ + .order_by('id') + + # Get display FARs for preferred_event + context.update(zip( + ['display_far', 'display_far_hr', 'far_is_upper_limit'], + self.get_display_far(obj=superevent.preferred_event) + ) + ) + + # Form to change GW status (only for authorized users) + # Only show if superevent is NOT a GW. Require manual intervention to + # revert since it will surely mess with automated numbering of date IDs + if not superevent.is_gw and self.request.user.has_perm( + 'confirm_gw_superevent'): + context['show_gw_status_form'] = True + else: + context['show_gw_status_form'] = False + + # 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(self.request.user) + + # Get list of EMGroup names for + context['emgroups'] = EMGroup.objects.all().order_by('name') \ + .values_list('name', flat=True) + + return context + + # Need to restrict ability to view -def webview(request, superevent_id): +def old_webview(request, superevent_id): # TODO: any special web displays for template for confirmed GWs? # can do this in template by checking superevent.is_gw @@ -33,10 +114,7 @@ def webview(request, superevent_id): context['preferred_event'] = superevent.preferred_event # Display far - if superevent.preferred_event is not None: - display_far = superevent.preferred_event.far - else: - display_far = None + 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: @@ -76,7 +154,9 @@ def webview(request, superevent_id): # Temporary method for getting logs context['logs'] = superevent.log_set.select_related('issuer').all() - return render(request, 'superevents/view.html', context=context) + + + return render(request, 'superevents/old_view.html', context=context) # Need to add an auth check for this too # If we use javascript for this eventually, we will want to enforce @@ -202,3 +282,162 @@ def file_download(request, superevent_id, filename): # Check file and serve it return check_and_serve_file(request, file_path, ResponseClass=HttpResponse) + + +# TODO: add permission checking +@require_POST +def modify_permissions(request, superevent_id): + + # Get superevent + superevent = get_superevent_by_date_id_or_404(request, superevent_id) + + # Get info from POST data + group_name = request.POST.get('group_name', None) + action = request.POST.get('action', None) + + if not group_name or not action: + msg = 'Modify_permissons requires both group_name and action in POST.' + return HttpResponseBadRequest(msg) + + # Get the group + try: + group = AuthGroup.objects.get(name=group_name) + 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 + if not request.user.has_perm('guardian.add_groupobjectpermission'): + 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) + + elif action == 'protect': + # Check permissions + if not request.user.has_perm('guardian.delete_groupobjectpermission'): + msg = "You aren't authorized to delete permission objects." + 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 + + else: + msg = "Unknown action. Choices are 'expose' and 'protect'." + return HttpResponseBadRequest(msg) + + # Redirect to original page, or home (if original page not found) + original_url = request.META.get('HTTP_REFERER', reverse('home')) + + return HttpResponseRedirect(original_url) + +@require_POST +def modify_signoff(request, superevent_id): + # Redirect to original page, or home (if original page not found) + original_url = request.META.get('HTTP_REFERER', reverse('home')) + + + # TODO: + # CHECK IF USER IS AUTHORIZED!!! + + + # Set up dict for passing to signoff form + signoff_dict = request.POST.copy() + signoff_dict['submitter'] = request.user.id + + # Get superevent id from date-based superevent_id + superevent = get_superevent_by_date_id_or_404(request, superevent_id) + signoff_dict['superevent'] = superevent.id + # TODO: + # After getting superevent, make sure user has appropriate permissions + # to operate on it + + # Get action and delete status + action = signoff_dict.get('action', None) + delete = signoff_dict.get('delete', None) + + # Use information in signoff_dict to check if a signoff already exists. + signoff = superevent.signoff_set.filter( + instrument=signoff_dict['instrument'], + signoff_type=signoff_dict['signoff_type']).first() + + # Error checking - use messages + error_msg = None + if action is None: + error_msg = "form action not found." + elif signoff is not None and action == 'CR': + error_msg = "a signoff already exists, please refresh the page." + elif signoff is None and action == 'UP': + error_msg = "signoff no longer exists, please refresh the page." + elif action == 'create' and delete is True: + error_msg = "can't delete signoff with creation form." + if error_msg is not None: + error_msg = "Error: " + error_msg + messages.error(request, error_msg) + return HttpResponseRedirect(original_url) + + # Check for delete parameter. If True, just delete the signoff. + if delete: + delete_signoff_for_superevent(signoff, request.user, + add_log_message=True, issue_alert=True) + messages.info(request, "Signoff deleted.") + return HttpResponseRedirect(original_url) + + # If so, get an update form + if signoff is not None: + form = SignoffForm(signoff_dict, instance=signoff) + else: + form = SignoffForm(signoff_dict) + + # Validate with form and create new log object ---------------------------- + + # If form is valid, create new log object from form data + error_msg = None + if form.is_valid(): + # Create/update signoff object + # Note that form.save() includes creating a log message and + # generating an alert + try: + obj = form.save() + except Exception as e: + error_msg = "Error saving signoff: {e}".format(e=e) + else: + if action == 'CR': + messages.info(request, 'Signoff created.') + elif action == 'UP': + messages.info(request, 'Signoff updated.') + + else: + # Send form errors to messaging + error_msg = "Invalid input: {e}".format(e=form.errors) + + if error_msg is not None: + logger.error(error_msg) + messages.error(request, error_msg) + + return HttpResponseRedirect(original_url) + diff --git a/gracedb/templates/superevents/detail.html b/gracedb/templates/superevents/detail.html new file mode 100644 index 0000000000000000000000000000000000000000..f2ee3be1ce2b6e5094882b0038b948193710ae91 --- /dev/null +++ b/gracedb/templates/superevents/detail.html @@ -0,0 +1,227 @@ +{% extends "base.html" %} +{% load timeutil %} +{% load scientific %} +{% load sanitize_html %} +{% load logtags %} +{% block heading %}{% endblock %} +{% block bodyattrs %}class="tundra eventDetail"{% endblock %} + +{% block jscript %} +{% load static %} +<link rel="stylesheet" href="{% static "css/labeltips.css" %}" /> +<script src="{% static "moment/moment.js" %}"></script> +<script src="{% static "moment-timezone/builds/moment-timezone-with-data.min.js" %}"></script> +<script src="{% static "dojo/dojo.js" %}" data-dojo-config="async: true"></script> +<script src="{% static "jquery/dist/jquery.min.js" %}"></script> +<!-- Styles for dgrid --> +<!-- <link rel="stylesheet" href="{% static "dgrid/css/dgrid.css" %}" /> --> +<!-- Styles for the editor components --> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/PageBreak.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/ShowBlockNodes.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/Preview.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/Save.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/Breadcrumb.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/FindReplace.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/PasteFromWord.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/InsertAnchor.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/CollapsibleToolbar.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/Blockquote.css" %}" /> +<link rel="stylesheet" href="{% static "dojox/editor/plugins/resources/css/Smiley.css" %}" /> +<!-- Styles for the lightboxes. --> +<link rel="stylesheet" href="{% static "dojox/image/resources/LightboxNano.css" %}" /> +<!-- Local style declarations --> +<link rel="stylesheet" href="{% static "dijit/themes/tundra/tundra.css" %}" /> + +<!-- the main JavaScript block is pulled in with an include --> +<script> +{% include "superevents/superevent_detail_script.js" %} +</script> + +{% endblock %} + +{% block content %} +<div id='event_detail_content'> + +{% if messages %} +<div class="flash"> + {% for m in messages %} + <p>{{ m }}</p> + {% endfor %} +</div> +{% endif %} + +{% if show_gw_status_form %} +<div class="content-area"> +<form action="{% url "superevents:confirm-gw" superevent.superevent_id %}" method="POST"> + <input type="submit" value="Confirm this superevent as a GW" class="permButtonClass"> +</form> +<div><b>Note:</b> this action is irreversible without manual intervention by an admin!</div> +</div> +{% endif %} + +{% 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 %} +<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> +</div> +{% endif %} + +{% if operator_signoff_authorized and operator_signoff_active %} +<div class="signoff-area"> + <h2>{{ operator_signoff_instrument }} Operator Signoff</h2> + <p>You are seeing this section because you've connected from a machine that, according to our records, is in the {{ signoff_instrument }} control room.</p> + {% 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> + {% endif %} + <form action="{% url "superevents:modify-signoff" superevent.superevent_id %}" method="POST"> + <table> + {{ operator_signoff_form.as_table }} + <tr><td></td><td><input type="submit" value="Submit" class="searchButtonClass"></td></tr> + </table> + </form> +</div> +{% endif %} + +<!-- 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> + <p>You are seeing this section because you're a designated EM followup advocate. + {% if advocate_signoff_exists %} + This event has already been signed off on. Use the form below if you wish to edit or delete the record. + {% else %} + This event still requires EM Followup advocate signoff. + {% endif %} + </p> + + <form action="{% url "superevents:modify-signoff" superevent.superevent_id %}" method="POST"> + <table> + {{ advocate_signoff_form.as_table }} + <tr><td></td><td><input type="submit" value="Submit" class="searchButtonClass"></td></tr> + </table> + </form> +</div> +{% endif %} + +{% endif %} {# lvem_view not in request.path #} + +<div class="content-area"> +{% 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> + <th>Links</th> + </tr> + <tr> + <td>{{ superevent.superevent_id }}</td> + <td>{% for labelling in superevent.labelling_set.all %} + <div onmouseover="tooltip.show(tooltiptext('{{labelling.label.name}}', '{{labelling.creator.username}}', '{{labelling.created|utc}}', '{{labelling.label.description}}'));" onmouseout="tooltip.hide();" 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> + <td><a href="{% url "superevents:file-list" superevent.superevent_id %}">Data</a></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 %} + <div onmouseover="tooltip.show(tooltiptext('{{labelling.label.name}}', '{{labelling.creator.username}}', '{{labelling.created|utc}}', '{{labelling.label.description}}'));" onmouseout="tooltip.hide();" style="color: {{labelling.label.defaultColor}}"><b>{{ labelling.label.name }}</b></div> + {% 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_hr }}</td> + <td><a href="{{ preferred_event.weburl }}">Data</a></td> + <td>{{ preferred_event.created|multiTime:"created" }}</td> + </tr> +</table> +{% endblock %} +{% include "superevents/emo_form_frag.html" %} + +</div> <!-- end event_detail_content div --> +{% endblock %} + diff --git a/gracedb/templates/superevents/emo_form_frag.html b/gracedb/templates/superevents/emo_form_frag.html new file mode 100644 index 0000000000000000000000000000000000000000..bacd86d39834caa7d374dec70a9891f4caca7b32 --- /dev/null +++ b/gracedb/templates/superevents/emo_form_frag.html @@ -0,0 +1,60 @@ +<div data-dojo-type="dijit/form/Form" id="emoFormContainer" +data-dojo-id="emoFormContainer" encType="multipart/form-data" action="" +method=""> +<script> + require(["dojo/parser", "dijit/form/Form", "dijit/form/Button", "dijit/form/ValidationTextBox", "dijit/form/DateTextBox"]); + </script> + +<script type="dojo/on" data-dojo-event="reset"> +return confirm('Press OK to reset widget values'); +</script> +<script type="dojo/on" data-dojo-event="submit"> +if(this.validate()){ +return confirm('Form is valid, press OK to submit'); +}else{ +alert('Form contains invalid data. Please correct first'); +return false; +} +return true; +</script> + +<form method="POST" id="emo_submit_form"> +<table> + +<tr><td><a href=# onclick="alert('Group with which the LSC has signed a trust agreement, hereby providing data +under its trust (required).'); return false;"> +Which MOU Group provides this report?</a></td> <td><select name="group"> +<option value=""></option> +{% for g in emgroups %} +<option value="{{ g }}">{{ g }}</option> +{% endfor %} </select> </td> </tr> + +<tr><td><a href=# onclick="alert('RA and Dec specify a center point of a rectangle that is aligned equatorially. Or list of centers. They must be in decimal degrees 0<=RA<=360 -90<=Dec<=90, in the J2000 frame.');return false;"> +RA (decimal degrees)</a></td> <td><input type="text" name="ra_list" value="" size=80/></td></tr> + +<tr><td><a href=# onclick="alert('RA and Dec specify a center point of a rectangle that is aligned equatorially. Or list of centers. They must be in decimal degrees 0<=RA<=360 -90<=Dec<=90, in the J2000 frame.');return false;"> +Dec (decimal degrees)</a></td> <td><input type="text" name="dec_list" value="" size=80/></td></tr> + +<tr><td><a href=# onclick="alert('RAWidth and DecWidth specify the size of a a rectangle that is aligned equatorially. Thus the edge of the box is distant from the center by half of the width.');return false;"> +RAwidth (decimal degrees)</a></td> <td><input type="text" name="ra_width_list" value=""/></td></tr> + +<tr><td><a href=# onclick="alert('RAWidth and DecWidth specify the size of a a rectangle that is aligned equatorially. Thus the edge of the box is distant from the center by half of the width.');return false;"> +Decwidth (decimal degrees)</a></td> <td><input type="text" name="dec_width_list" value=""/></td></tr> + +<tr><td><a href=# onclick="alert('The time at the beginning of a time interval during which the observation was taken. Or list of times. UTC in ISO 8601 format.');return false;"> +StartTime</a></td> +<td><input type="text" name="start_time_list" value="" size=80/></td> +</tr> + +<tr><td><a href=# onclick="alert('This is the on-source exposure time in seconds (or the duration of the observation).');return false;"> +On source exposure (seconds)</a></td> <td><input type="text" name="duration_list" value=""/></td></tr> + +<tr><td><a href=# onclick="alert('A natural language report.');return false;"> +Report as text</a></td> <td colspan=2><textarea name="comment" rows="8" cols="50"></textarea></td></tr> + +</table> +<input type="submit" value="Submit EMBB Observation Report"/> +</form> + +</div> + diff --git a/gracedb/templates/superevents/view.html b/gracedb/templates/superevents/old_view.html similarity index 100% rename from gracedb/templates/superevents/view.html rename to gracedb/templates/superevents/old_view.html diff --git a/gracedb/templates/superevents/superevent_detail_script.js b/gracedb/templates/superevents/superevent_detail_script.js new file mode 100644 index 0000000000000000000000000000000000000000..699c2173911f86e2c0774488e088f606a26357aa --- /dev/null +++ b/gracedb/templates/superevents/superevent_detail_script.js @@ -0,0 +1,1281 @@ +// Ugh. Why do I have to pull the stuff in here? + +// Constructs text for label tooltips +function tooltiptext(name, creator, time, description) { + //return ( creator + " " + time + "<br/>" + label_descriptions[name] ); + return ( creator + " (" + time + "): " + description ); +}; +var tooltip=function(){ + var id = 'tt'; + var top = 3; + var left = 3; + var maxw = 300; + var speed = 10; + var timer = 20; + var endalpha = 95; + var alpha = 0; + var tt,t,c,b,h; + var ie = document.all ? true : false; + return{ + show:function(v,w){ + if(tt == null){ + tt = document.createElement('div'); + tt.setAttribute('id',id); + t = document.createElement('div'); + t.setAttribute('id',id + 'top'); + c = document.createElement('div'); + c.setAttribute('id',id + 'cont'); + b = document.createElement('div'); + b.setAttribute('id',id + 'bot'); + tt.appendChild(t); + tt.appendChild(c); + tt.appendChild(b); + document.body.appendChild(tt); + tt.style.opacity = 0; + tt.style.filter = 'alpha(opacity=0)'; + document.onmousemove = this.pos; + } + tt.style.display = 'block'; + c.innerHTML = v; + tt.style.width = w ? w + 'px' : 'auto'; + if(!w && ie){ + t.style.display = 'none'; + b.style.display = 'none'; + tt.style.width = tt.offsetWidth; + t.style.display = 'block'; + b.style.display = 'block'; + } + if(tt.offsetWidth > maxw){tt.style.width = maxw + 'px'} + h = parseInt(tt.offsetHeight) + top; + clearInterval(tt.timer); + tt.timer = setInterval(function(){tooltip.fade(1)},timer); + }, + pos:function(e){ + var u = ie ? event.clientY + document.documentElement.scrollTop : e.pageY; + var l = ie ? event.clientX + document.documentElement.scrollLeft : e.pageX; + tt.style.top = (u - h) + 'px'; + tt.style.left = (l + left) + 'px'; + }, + fade:function(d){ + var a = alpha; + if((a != endalpha && d == 1) || (a != 0 && d == -1)){ + var i = speed; + if(endalpha - a < speed && d == 1){ + i = endalpha - a; + }else if(alpha < speed && d == -1){ + i = a; + } + alpha = a + (i * d); + tt.style.opacity = alpha * .01; + tt.style.filter = 'alpha(opacity=' + alpha + ')'; + }else{ + clearInterval(tt.timer); + if(d == -1){tt.style.display = 'none'} + } + }, + hide:function(){ + clearInterval(tt.timer); + tt.timer = setInterval(function(){tooltip.fade(-1)},timer); + } + }; +}(); + + +// This should probably also go somewhere else. +// Closure +(function() { + /** + * Decimal adjustment of a number. + * + * @param {String} type The type of adjustment. + * @param {Number} value The number. + * @param {Integer} exp The exponent (the 10 logarithm of the adjustment base). + * @returns {Number} The adjusted value. + */ + function decimalAdjust(type, value, exp) { + // If the exp is undefined or zero... + if (typeof exp === 'undefined' || +exp === 0) { + return Math[type](value); + } + value = +value; + exp = +exp; + // If the value is not a number or the exp is not an integer... + if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { + return NaN; + } + // Shift + value = value.toString().split('e'); + value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); + // Shift back + value = value.toString().split('e'); + return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); + } + + // Decimal round + if (!Math.round10) { + Math.round10 = function(value, exp) { + return decimalAdjust('round', value, exp); + }; + } + // Decimal floor + if (!Math.floor10) { + Math.floor10 = function(value, exp) { + return decimalAdjust('floor', value, exp); + }; + } + // Decimal ceil + if (!Math.ceil10) { + Math.ceil10 = function(value, exp) { + return decimalAdjust('ceil', value, exp); + }; + } +})(); + + +// A utility +var getKeys = function(obj){ + var keys = []; + for(var key in obj){ + keys.push(key); + } + return keys; +} + +var image_extensions = ['png', 'gif', 'jpg']; +var TIME_DISP_FMT = 'MMM D, YYYY h:mm:ss A'; +var UTC_TIME_DISP_FMT = 'MMM D, YYYY HH:mm:ss UTC'; +//var TIME_DISP_FMT = 'LLL'; + +// A utility function to determine whether a log message has an image. +// This would not be necessary if we were using django template language +var hasImage = function(object) { + if (!object.filename) return false; + var file_extension = object.filename.slice(object.filename.length - 3); + return image_extensions.indexOf(file_extension) >= 0; +} + +// some URLs. Usage of Django template syntax should be limited to here +var tagListUrl = '{% url "shib:tag-list" %}'; +var tagCreateUrlPattern = '{% url "shib:superevents:superevent-log-tag-list" superevent.superevent_id "000" %}'; +var tagDeleteUrlPattern = '{% url "shib:superevents:superevent-log-tag-detail" superevent.superevent_id "000" "FAKE_TAG_NAME" %}'; +var logListUrl = '{% url "shib:superevents:superevent-log-list" superevent.superevent_id %}'; +var logSaveUrl = '{% url "shib:superevents:superevent-log-list" superevent.superevent_id %}'; +var emObservationListUrl = '{% url "shib:superevents:superevent-emobservation-list" superevent.superevent_id %}'; +var fileDownloadUrl = '{% url "superevents:file-download" superevent.superevent_id "FAKE_FILE_NAME" %}'; +var skymapViewerUrl = '{{ SKYMAP_VIEWER_SERVICE_URL }}'; + +// This little list determines the priority ordering of the digest sections. +var blessed_tag_priority_order = [ + 'analyst_comments', + 'psd', + 'data_quality', + 'sky_loc', + 'background', + 'ext_coinc', + 'strain', + 'tfplots', + 'sig_info', + 'audio', +]; + +require([ + 'dojo/_base/declare', + 'dojo/query', + 'dojo/on', + 'dojo/parser', + 'dojo/dom', + 'dojo/dom-construct', + 'dojo/dom-style', + 'dojo/request', + 'dojo/store/Memory', + 'dojo/data/ObjectStore', + 'dstore/Rest', + 'dstore/RequestMemory', + 'dgrid/Grid', + 'dgrid/extensions/DijitRegistry', + 'put-selector/put', + 'dijit/TitlePane', + 'dijit/form/Form', + 'dijit/form/Button', + 'dijit/form/TextBox', + 'dijit/form/ComboBox', + 'dijit/form/Select', + 'dijit/Tooltip', + 'dijit/Dialog', + 'dijit/Editor', + 'dojox/editor/plugins/Save', + 'dojox/editor/plugins/Preview', + 'dojox/layout/ScrollPane', + 'dojox/form/Uploader', +// 'dojox/form/uploader/plugins/HTML5', + 'dojox/form/uploader/plugins/IFrame', + 'dojox/image/LightboxNano', + 'dijit/_editor/plugins/TextColor', + 'dijit/_editor/plugins/LinkDialog', + 'dijit/_editor/plugins/ViewSource', + 'dijit/_editor/plugins/NewPage', + 'dijit/_editor/plugins/FullScreen', + 'dojo/domReady!', +], function(declare, query, on, parser, dom, domConstruct, domStyle, request, Memory, ObjectStore, + Rest, RequestMemory, Grid, DijitRegistry, + put, + TitlePane, Form, Button, TextBox, ComboBox, Select, Tooltip, Dialog, Editor, + Save, Preview, ScrollPane, Uploader) { + + parser.parse(); + //---------------------------------------------------------------------------------------- + // Some utility functions + //---------------------------------------------------------------------------------------- + var createExpandingSection = function (titleNode, contentNode, formNode, titleText, initiallyOpen) { + + // Instead let's make a table. + var titleTableRow = put(titleNode, "table tr"); + var expandGlyphNode = put(titleTableRow, "td.title div.expandGlyph"); + var titleTextNode = put(titleTableRow, "td.title h2", titleText); + var addButtonNode = put(titleTableRow, "td.title div.expandFormButton", '(add)'); + + if (!(initiallyOpen && initiallyOpen==true)) { + put(expandGlyphNode, '.closed'); + domStyle.set(contentNode, 'display', 'none'); + domStyle.set(addButtonNode, 'display', 'none'); + } + // This one is always closed initially + domStyle.set(formNode, 'display', 'none'); + + on(expandGlyphNode, "click", function() { + if (domStyle.get(contentNode, 'display') == 'none') { + domStyle.set(contentNode, 'display', 'block'); + domStyle.set(addButtonNode, 'display', 'block'); + put(expandGlyphNode, '!closed'); + } else { + domStyle.set(contentNode, 'display', 'none'); + domStyle.set(addButtonNode, 'display', 'none'); + put(expandGlyphNode, '.closed'); + } + }); + + on(titleTextNode, "click", function() { + if (domStyle.get(contentNode, 'display') == 'none') { + domStyle.set(contentNode, 'display', 'block'); + domStyle.set(addButtonNode, 'display', 'block'); + put(expandGlyphNode, '!closed'); + } else { + domStyle.set(contentNode, 'display', 'none'); + domStyle.set(addButtonNode, 'display', 'none'); + put(expandGlyphNode, '.closed'); + } + }); + + on(addButtonNode, "click", function() { + if (domStyle.get(formNode, 'display') == 'none') { + domStyle.set(formNode, 'display', 'block'); + addButtonNode.innerHTML = '(cancel)'; + } else { + domStyle.set(formNode, 'display', 'none'); + addButtonNode.innerHTML = '(add)'; + } + }); + } + + var createExpandingSectionNoForm = function (titleNode, contentNode, titleText, initiallyOpen) { + // Instead let's make a table. + var titleTableRow = put(titleNode, "table tr"); + var expandGlyphNode = put(titleTableRow, "td.title div.expandGlyph"); + var titleTextNode = put(titleTableRow, "td.title h2", titleText); + + if (!(initiallyOpen && initiallyOpen==true)) { + put(expandGlyphNode, '.closed'); + domStyle.set(contentNode, 'display', 'none'); + } + + on(expandGlyphNode, "click", function() { + if (domStyle.get(contentNode, 'display') == 'none') { + domStyle.set(contentNode, 'display', 'block'); + put(expandGlyphNode, '!closed'); + } else { + domStyle.set(contentNode, 'display', 'none'); + put(expandGlyphNode, '.closed'); + } + }); + + on(titleTextNode, "click", function() { + if (domStyle.get(contentNode, 'display') == 'none') { + domStyle.set(contentNode, 'display', 'block'); + put(expandGlyphNode, '!closed'); + } else { + domStyle.set(contentNode, 'display', 'none'); + put(expandGlyphNode, '.closed'); + } + }); + } + + var timeChoicesData = [ + {"id": "llo", "label": "LLO Local"}, + {"id": "lho", "label": "LHO Local"}, + {"id": "virgo", "label": "Virgo Local"}, + {"id": "utc", "label": "UTC"}, + ]; + // XXX Fixme. So. Bad. + var timeChoicesDataWithGps = [ + {"id": "gps", "label": "GPS Time"}, + {"id": "llo", "label": "LLO Local"}, + {"id": "lho", "label": "LHO Local"}, + {"id": "virgo", "label": "Virgo Local"}, + {"id": "utc", "label": "UTC"}, + ]; + + var timeChoices = new Memory ({ data: timeChoicesData }); + var timeChoicesStore = new ObjectStore({ objectStore: timeChoices}); + var timeChoicesWithGps = new Memory ({ data: timeChoicesDataWithGps }); + var timeChoicesWithGpsStore = new ObjectStore({ objectStore: timeChoicesWithGps}); + + var createTimeSelect = function(node, label, defaultName, useGps) { + var myStore = (useGps) ? timeChoicesWithGpsStore : timeChoicesStore; + var s = new Select({ store: myStore }, node); + s.attr("value", defaultName); + s.on("change", function () { changeTime(this, label); }); + return s; + } + + //---------------------------------------------------------------------------------------- + // Take care of stray time selects + //---------------------------------------------------------------------------------------- + createTimeSelect(dom.byId('basic_info_event_ts'), 'gps', 'gps', true); + createTimeSelect(dom.byId('basic_info_created_ts'), 'created', 'utc', true); + createTimeSelect(dom.byId('neighbors_event_ts'), 'ngps', 'gps', true); + createTimeSelect(dom.byId('neighbors_created_ts'), 'ncreated', 'utc', true); + + //---------------------------------------------------------------------------------------- + // Section for EMBB + //---------------------------------------------------------------------------------------- + var eventDetailContainer = dom.byId('event_detail_content'); + //var embbDiv = put(eventDetailContainer, 'div.content-area#embb_container'); + //var embbTitleDiv = put(embbDiv, 'div#embb_title_expander'); + //var embbContentDiv = put(embbDiv, 'div#embb_content'); + + // Put the EEL form into the content div + // FIXME This needs to be cleaned up. Empty div for now. + //var oldEelFormDiv = dom.byId('eelFormContainer'); + //var eelFormContents = oldEelFormDiv.innerHTML; + //var oldEmoFormDiv = dom.byId('emoFormContainer'); + //var emoFormContents = oldEmoFormDiv.innerHTML; + // domConstruct.destroy('eelFormContainer'); + //domConstruct.destroy('emoFormContainer'); + // var embbAddDiv = put(embbContentDiv, 'div#add_eel_container'); + /* var embbAddFormDiv = put(embbAddDiv, 'div#add_eel_form_container'); + embbAddFormDiv.innerHTML = eelFormContents; */ + //var emoAddDiv = put(embbContentDiv, 'div#add_emo_container'); + //var emoAddFormDiv = put(emoAddDiv, 'div#add_emo_form_container'); + //emoAddFormDiv.innerHTML = emoFormContents; + + //createExpandingSection(embbTitleDiv, embbContentDiv, emoAddFormDiv, 'Electromagnetic Bulletin Board'); + + // Append the div that will hold our dgrid + //put(embbContentDiv, 'div#emo-grid'); + + //---------------------------------------------------------------------------------------- + // Section for log entries + //---------------------------------------------------------------------------------------- + var annotationsDiv = put(eventDetailContainer, 'div.content-area'); + var logTitleDiv = put(annotationsDiv, 'div#log_title_expander'); + var logContentDiv = put(annotationsDiv, 'div#log_content'); + + // Create the form for adding a new log entry. + var logAddDiv = put(logContentDiv, 'div#new_log_entry_form'); + put(logAddDiv, 'div#previewer'); + put(logAddDiv, 'div#editor'); + put(logAddDiv, 'div#upload_form_container'); + + // Create handlers for upload success and failture + var uploadSuccess = function(result) { + alert(result); + }; + var uploadError = function(error) { + alert(error); + }; + + createExpandingSection(logTitleDiv, logContentDiv, logAddDiv, 'Superevent Log Messages', true); + + //---------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------- + // Get the tag properties. Sorta hate it that this is so complicated. + tagStore = new declare([Rest, RequestMemory])({target: tagListUrl}); + tagStore.get('').then(function(content) { + var tags = content.tags; + + var tag_display_names = new Object(); + var blessed_tags = new Array(); + for (var tag_name in tags) { + var tag = tags[tag_name]; + tag_display_names[tag_name] = tag.displayName; + if (tag.blessed) blessed_tags.push({ name: tag_name }); + } + // Reorder the blessed tags according to the priority order above. + var new_blessed_tags = new Array(); + for (var i=0; i<blessed_tag_priority_order.length; i++) { + var tag_name = blessed_tag_priority_order[i]; + for (var j=0; j<blessed_tags.length; j++) { + if (blessed_tags[j].name == tag_name) { + new_blessed_tags.push(blessed_tags[j]); + break; + } + } + } + // Add the rest of them. + for (var i=0; i<blessed_tags.length; i++) { + if (new_blessed_tags.indexOf(blessed_tags[i]) < 0) { + new_blessed_tags.push(blessed_tags[i]); + } + + } + blessed_tags = new_blessed_tags; + var blessed_tag_names = blessed_tags.map(function (obj) { return obj.name; }); + var blessedTagStore = new Memory({ data: blessed_tags }); + + // Create the tag callback generators. These don't depend on the log message contents + // so we should be able to define them here. + function getTagDelCallback(tag_name, N) { + return function() { + // Wonky replacement so we don't replace 000s in graceid or somewhere else + // where we don't want to do that. + tagUrl = tagDeleteUrlPattern.replace("/000/", "/"+N+"/").replace("FAKE_TAG_NAME",encodeURIComponent(tag_name)); + var tagResultDialog = new Dialog({ style: "width: 300px" }); + var actionBar = domConstruct.create("div", { "class": "dijitDialogPaneActionBar" }); + var tbnode = domConstruct.create("div", { + style: "margin: 0px auto 0px auto; text-align: center;" + }, actionBar); + var reload_page = false; + var tagButton = new Button({ + label: "Ok", + onClick: function(){ + tagResultDialog.hide(); + if (reload_page) { location.reload(true); } + }}).placeAt(tbnode); + request.del(tagUrl).then( + function(text){ + tagResultDialog.set("content", "Removed tag " + tag_name + " for message " + N + "."); + domConstruct.place(actionBar, tagResultDialog.containerNode); + tagResultDialog.show(); + reload_page = true; + }, + function(error){ + var err_msg = error; + if (error.response.text) { err_msg = error.response.text; } + tagResultDialog.set("content", "Error: " + err_msg); + domConstruct.place(actionBar, tagResultDialog.containerNode); + tagResultDialog.show(); + reload_page = false; + }); + } + } + + function getTagAddCallback(N) { + return function() { + // Create the tag result dialog. + var tagResultDialog = new Dialog({ style: "width: 300px" }); + var actionBar = domConstruct.create("div", { "class": "dijitDialogPaneActionBar" }); + var tbnode = domConstruct.create("div", { + style: "margin: 0px auto 0px auto; text-align: center;" + }, actionBar); + var reload_page = false; + var tagButton = new Button({ + label: "Ok", + onClick: function(){ + tagResultDialog.hide(); + if (reload_page) { location.reload(true); } + } + }).placeAt(tbnode); + + // Create the form + addTagForm = new Form(); + var msg = "<p> Choose a tag \ + name from the dropdown menu or enter a new one. If you are \ + creating a new tag, please also provide a display name. </p>"; + domConstruct.create("div", {innerHTML: msg} , addTagForm.containerNode); + + // Form for tagging existing log messages. + new ComboBox({ + name: "existingTagSelect", + value: "", + store: blessedTagStore, + searchAttr: "name" + }).placeAt(addTagForm.containerNode); + + new TextBox({ + name: "tagDispName", + }).placeAt(addTagForm.containerNode); + + new Button({ + type: "submit", + label: "OK", + }).placeAt(addTagForm.containerNode); + + // Create the dialoge + addTagDialog = new Dialog({ + title: "Add Tag", + content: addTagForm, + style: "width: 300px" + }); + + // Define the form on submit handler + on(addTagForm, "submit", function(evt) { + evt.stopPropagation(); + evt.preventDefault(); + formData = addTagForm.getValues(); + var tagName = formData.existingTagSelect; + var tagDispName = formData.tagDispName; + // Wonky replacement so we don't replace 000s in graceid or somewhere else + // where we don't want to do that. + var tagUrl = tagCreateUrlPattern.replace("/000/", "/"+N+"/"); + + request.post(tagUrl, { + data: { + name: tagName, + displayName: tagDispName + } + }).then( + function(text){ + tagResultDialog.set("content", "Successfully applied tag " + tagName + " to log message " + N + "."); + domConstruct.place(actionBar, tagResultDialog.containerNode); + tagResultDialog.show(); + reload_page = true; + }, + function(error){ + var err_msg = error; + if (error.response.text) { err_msg = error.response.text; } + tagResultDialog.set("content", "Error: " + err_msg); + domConstruct.place(actionBar, tagResultDialog.containerNode); + tagResultDialog.show(); + reload_page = false; + } + ); + addTagDialog.hide(); + }); + + // show the dialog + addTagDialog.show(); + } + } + + //---------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------- + // Now that we've got the tag info, let's get the event log objects. + logStore = new declare([Rest, RequestMemory])({target: logListUrl}); + logStore.get('').then(function(content) { + + // Pull the logs out of the JSON returned by the server. + var logs = content.log; + var Nlogs = logs.length; + + // Convert the 'created' times to UTC. + logs = logs.map( function(obj) { + var server_t = moment.tz(obj.created, 'UTC'); + obj.created = server_t.clone().tz('UTC').format(UTC_TIME_DISP_FMT); + return obj; + }); + + // Total up the tags present. This list will have duplicates. + var total_tags = new Array(); + logs.forEach( function(log) { + log.tag_names.forEach( function (tag_name) { + total_tags.push(tag_name); + }); + }); + + // Figure out what blessed tags are present. + var our_blessed_tags = blessed_tag_names.filter( function(value) { + return total_tags.indexOf(value) >= 0; + }); + + var skymap_stems = new Array(); + logs.forEach( function(log) { + if (log.tag_names.indexOf('sky_loc') >= 0 && log.filename.indexOf('.fits') >= 0) { + skymap_stems.push(log.filename.slice(0,log.filename.indexOf('.fits'))); + } + }); + + var isJsonSkymap = function(filename) { + var is_skymap = false; + if (filename.indexOf('.json') >= 0) { + skymap_stems.forEach( function(stem) { + if (filename.indexOf(stem) >= 0) { + is_skymap = true; + } + }); + } + return is_skymap; + }; + + // If there are any blessed tags here, we'll do TitlePanes + if (our_blessed_tags.length > 0) { + // define our columns for the topical digest panes + var columns = [ + { + field: 'created', + renderHeaderCell: function(node) { + timeHeaderContainer = put(node, 'div'); + createTimeSelect(timeHeaderContainer, 'log', 'utc'); + put(timeHeaderContainer, 'div', 'Log Entry Created'); + return timeHeaderContainer; + }, + renderCell: function(object, value, node, options) { + var server_t = moment.tz(object.created, 'UTC'); + var t = put(node, 'time[name="time-log"]', server_t.format(UTC_TIME_DISP_FMT)); + put(t, '[utc="$"]', server_t.clone().tz('UTC').format(UTC_TIME_DISP_FMT)); + put(t, '[llo="$"]', server_t.clone().tz('America/Chicago').format(TIME_DISP_FMT)); + put(t, '[lho="$"]', server_t.clone().tz('America/Los_Angeles').format(TIME_DISP_FMT)); + put(t, '[virgo="$"]', server_t.clone().tz('Europe/Rome').format(TIME_DISP_FMT)); + return t; + } + }, + { field: 'issuer', label: 'Submitter', get: function(obj) { return obj.issuer; } }, + // Sometimes the comment contains HTML, so we just want to return whatever it has. + // This is where the link with the filename goes. Also the view in skymapViewer button + { + field: 'comment', + label: 'Comment', + renderCell: function(object, value, node, options) { + var commentDiv = put(node, 'div'); + // Putting this in the innerHTML allows users to create comments in HTML. + // Whereas, inserting the comment with the put selector escapes it. + commentDiv.innerHTML += value + ' '; + if (object.filename) put(commentDiv, 'a[href=$]', fileDownloadUrl.replace("FAKE_FILE_NAME", object.filename), object.filename); + // Branson, 3/3/15 + //if (object.filename == 'skymap.json') { + var isItJson = object.filename.indexOf(".json"); + //if (isItJson > -1) { + if (isJsonSkymap(object.filename)) { + var skymapName = object.filename.substring(0, isItJson); + var svButton = put(commentDiv, + 'button.modButtonClass.sV_button#'+skymapName, 'View in SkymapViewer!'); + put(svButton, '[type="button"][data-dojo-type="dijit/form/Button"]'); + put(svButton, '[style="float: right"]'); + } + return commentDiv; + } + }, + + ]; + + // Create the topical digest title panes + for (i=0; i<our_blessed_tags.length; i++) { + var tag_name = our_blessed_tags[i]; + // First filter the log messages based on whether they have this blessed tag. + var tagLogs = logs.filter( function(obj) { + // XXX Not sure why this simpler filter didn't work. + // return obj.tag_names.indexOf(tag_name) > 0; + for (var k=0; k<obj.tag_names.length; k++) { + if (obj.tag_names[k]==tag_name) return true; + } + return false; + }); + + // Next filter the remaining log messages based on whether or not images are present. + var imgLogs = tagLogs.filter( function(obj) { return hasImage(obj); }); + var noImgLogs = tagLogs.filter ( function(obj) { return !hasImage(obj); }); + + // Create the title pane with a placeholder div + var pane_contents_id = tag_name.replace(/ /g,"_") + '_pane'; + var tp = new TitlePane({ + title: tag_display_names[tag_name], + content: '<div id="' + pane_contents_id + '"></div>', + open: true + }); + logContentDiv.appendChild(tp.domNode); + paneContentsNode = dom.byId(pane_contents_id); + + // Handle the log messages with images by putting them in little box. + if (imgLogs.length) { + var figContainerId = tag_name.replace(/ /g,"_") + '_figure_container'; + var figDiv = put(paneContentsNode, 'div#' + figContainerId); + // Instead of living in a table row, the figures will now just be placed + // directly into the div, in inline blocks. + //var figRow = put(figDiv, 'table.figure_container tr'); + for (j=0; j<imgLogs.length; j++) { + var log = imgLogs[j]; + //var figTabInner = put(figRow, 'td table.figures'); + var figTabInner = put(figDiv, 'table.figures'); + var figA = put(figTabInner, 'tr.figrow img[height="180"][src=$]', log.file); + new dojox.image.LightboxNano({href: log.file}, figA); + var figComment = put(figTabInner, 'tr td'); + figComment.innerHTML = log.comment; + figComment.innerHTML += ' <a href="' + log.file + '">' + log.filename + '.</a> '; + figComment.innerHTML += 'Submitted by ' + log.issuer + ' on ' + log.created; + } + // XXX Have commented out the scroll pane at Patrick's request. + // var sp = new dojox.layout.ScrollPane({ orientation: "horizontal", style: "overflow: hidden;" }, figContainerId); + + } + + // Handle the log messages without images by putting them in a grid. + if (noImgLogs.length) { + var gridNode = put(paneContentsNode, 'div#' + pane_contents_id + '-grid') + var grid = new declare([Grid, DijitRegistry])({ + columns: columns, + className: 'dgrid-autoheight', + // Overriding renderRow here to add an extra class to the row. This is for styling. + renderRow: function(object,options) { + return put('div.supergrid-row', Grid.prototype.renderRow.call(this,object,options)); + } + }, gridNode); + grid.renderArray(noImgLogs); + grid.set("sort", 'N', descending=true); + } + + } + + // Create the EMObservations title pane + // XXX Branson fixme + var pane_contents_id = 'emobservations_pane_div'; + + // Create the title pane with a placeholder div + var emo_tp = new TitlePane({ + title: 'EM Observations', + content: '<div id="' + pane_contents_id + '"></div>', + open: true + }); + logContentDiv.appendChild(emo_tp.domNode); + + var emoDiv = dom.byId(pane_contents_id); + // Create the section for adding EMObservation records. First an outer container: + var emoAddDiv = put(emoDiv, 'div#add_emo_container'); + // The order is important. First the toggling form display button, then the form. + // FIXME: Such a sad way of putting in vertical space. + put(emoDiv, 'br') + var addEmoButtonNode = put(emoAddDiv, 'div.expandFormButton', '(add observation record)'); + var emoAddFormDiv = put(emoAddDiv, 'div#add_emo_form_container'); + domStyle.set(emoAddFormDiv, 'display', 'none'); + + on(addEmoButtonNode, "click", function() { + if (domStyle.get(emoAddFormDiv, 'display') == 'none') { + domStyle.set(emoAddFormDiv, 'display', 'block'); + addEmoButtonNode.innerHTML = '(cancel)'; + } else { + domStyle.set(emoAddFormDiv, 'display', 'none'); + addEmoButtonNode.innerHTML = '(add observation record)'; + } + }); + + // Grab the form fragment and put it in the right place. + var oldEmoFormDiv = dom.byId('emoFormContainer'); + var emoFormContents = oldEmoFormDiv.innerHTML; + domConstruct.destroy('emoFormContainer'); + emoAddFormDiv.innerHTML = emoFormContents; + + // Create the div for our grid to attach to + put(emoDiv, 'div#emo-grid'); + + // Create the full event-log title pane + var columns = [ + { field: 'N', label: 'No.' }, + { + field: 'created', + renderHeaderCell: function(node) { + timeHeaderContainer = put(node, 'div'); + var ts = createTimeSelect(timeHeaderContainer, 'audit-log', 'utc'); + put(timeHeaderContainer, 'div', 'Log Entry Created'); + // XXX Not sure how to get this to do the right thing. + return timeHeaderContainer; + //return ts; + }, + renderCell: function(object, value, node, options) { + var server_t = moment.tz(object.created, 'UTC'); + var t = put(node, 'time[name="time-audit-log"]', server_t.format(UTC_TIME_DISP_FMT)); + put(t, '[utc="$"]', server_t.clone().tz('UTC').format(UTC_TIME_DISP_FMT)); + put(t, '[llo="$"]', server_t.clone().tz('America/Chicago').format(TIME_DISP_FMT)); + put(t, '[lho="$"]', server_t.clone().tz('America/Los_Angeles').format(TIME_DISP_FMT)); + put(t, '[virgo="$"]', server_t.clone().tz('Europe/Rome').format(TIME_DISP_FMT)); + return t; + } + }, +{ field: 'issuer', label: 'Submitter', get: function(obj) { return obj.issuer; } }, + // Sometimes the comment contains HTML, so we just want to return whatever it has. + // This is where the link with the filename goes. Also the view in skymapViewer button + { + field: 'comment', + label: 'Comment', + renderCell: function(object, value, node, options) { + commentDiv = put(node, 'div'); + // Putting this in the innerHTML allows users to create comments in HTML. + // Whereas, inserting the comment with the put selector escapes it. + commentDiv.innerHTML += value + ' '; + if (object.filename) put(commentDiv, 'a[href=$]', fileDownloadUrl.replace("FAKE_FILE_NAME", object.filename), object.filename); + // Create tag-related features + var tagButtonContainer = put(commentDiv, 'div.tagButtonContainerClass'); + // For each existing tag on a log message, we will make a little widget + // to delete it. + object.tag_names.forEach( function(tag_name) { + var delDiv = put(tagButtonContainer, 'div.tagDelButtonDivClass'); + var del_button_id = "del_button_" + object.N + '_' + tag_name.replace(/ /g, "_"); + var delButton = put(delDiv, 'button.modButtonClass.left#' + del_button_id); + put(delButton, '[data-dojo-type="dijit/form/Button"]'); + // It looks like an 'x', so people will know that this means 'delete' + delButton.innerHTML = '×'; + var labButton = put(delDiv, 'button.modButtonClass.right', tag_name); + }); + // Create a button for adding a new tag. + var add_button_id = 'addtag_' + object.N; + var addButton = put(tagButtonContainer, 'button.modButtonClass#' + add_button_id); + put(addButton, '[data-dojo-type="dijit/form/Button"]'); + // Put a plus sign in there. + addButton.innerHTML = '+'; + + // The div is finally ready. Return it. + return commentDiv; + } + }, + { + field: 'image', + label: ' ', + renderCell: function(object, value, node, options) { + if (value) { + imgNode = put(node, 'img[height="60"][src="$"]', value); + return new dojox.image.LightboxNano({ href: value }, imgNode); + } + }, + get: function(object) { + if (hasImage(object)) { + return object.file; + } else { + return null; + } + }, + } + ]; + + var pane_contents_id = 'full_log_pane_div'; + + // Create the title pane with a placeholder div + var tp = new TitlePane({ + title: 'Full Superevent Log', + content: '<div id="' + pane_contents_id + '"></div>', + //open: false + open: true + }); + logContentDiv.appendChild(tp.domNode); + + var grid = new declare([Grid, DijitRegistry])({ + minRowsPerPage: Nlogs, + columns: columns, + className: 'dgrid-autoheight', + renderRow: function(object,options) { + return put('div.supergrid-row', Grid.prototype.renderRow.call(this,object,options)); + } + }, pane_contents_id); + grid.renderArray(logs); + grid.set("sort", 'N', descending=true); + + // Now that we've constructed it, let's close the title pane. + tp.toggle() + + } else { + // Not doing title panes, just put up the usual log message section. + // Will have the full eventlog section. Same as above, except that it + // won't be in a title pane. What is the best way to do this. + + // If we're not doing title panes, we still need to remember to destroy + // the emoFormContainer. Otherwise it shows up! + domConstruct.destroy('emoFormContainer'); + + var columns = [ + { field: 'N', label: 'No.' }, + { field: 'created', label: 'Log Entry Created' }, + { field: 'issuer', label: 'Submitter', get: function(obj) { return obj.issuer; } }, + // Sometimes the comment contains HTML, so we just want to return whatever it has. + // This is where the link with the filename goes. Also the view in skymapViewer button + { + field: 'comment', + label: 'Comment', + renderCell: function(object, value, node, options) { + commentDiv = put(node, 'div'); + // Putting this in the innerHTML allows users to create comments in HTML. + // Whereas, inserting the comment with the put selector escapes it. + commentDiv.innerHTML += value + ' '; + if (object.filename) put(commentDiv, 'a[href=$]', fileDownloadUrl.replace("FAKE_FILE_NAME", object.filename), object.filename); + // Create tag-related features + var tagButtonContainer = put(commentDiv, 'div.tagButtonContainerClass'); + // For each existing tag on a log message, we will make a little widget + // to delete it. + object.tag_names.forEach( function(tag_name) { + var delDiv = put(tagButtonContainer, 'div.tagDelButtonDivClass'); + var del_button_id = "del_button_" + object.N + '_' + tag_name.replace(/ /g, "_"); + var delButton = put(delDiv, 'button.modButtonClass.left#' + del_button_id); + put(delButton, '[data-dojo-type="dijit/form/Button"]'); + // It looks like an 'x', so people will know that this means 'delete' + delButton.innerHTML = '×'; + var labButton = put(delDiv, 'button.modButtonClass.right', tag_name); + }); + // Create a button for adding a new tag. + var add_button_id = 'addtag_' + object.N; + var addButton = put(tagButtonContainer, 'button.modButtonClass#' + add_button_id); + put(addButton, '[data-dojo-type="dijit/form/Button"]'); + // Put a plus sign in there. + addButton.innerHTML = '+'; + + // The div is finally ready. Return it. + return commentDiv; + } + }, + { + field: 'image', + label: ' ', + renderCell: function(object, value, node, options) { + if (value) { + imgNode = put(node, 'img[height="60"][src="$"]', value); + return new dojox.image.LightboxNano({ href: value }, imgNode); + } + }, + get: function(object) { + if (hasImage(object)) { + return object.file; + } else { + return null; + } + }, + } + ]; + + var grid = new declare([Grid, DijitRegistry])({ + minRowsPerPage: Nlogs, + columns: columns, + className: 'dgrid-autoheight', + renderRow: function(object,options) { + return put('div.supergrid-row', Grid.prototype.renderRow.call(this,object,options)); + } + }, logContentDiv); + grid.renderArray(logs); + grid.set("sort", 'N', descending=true); + + } + + //------------------------------------------------------------------- + // Finally, let's see if we can get those EMOs in + //------------------------------------------------------------------- + emoStore = new declare([Rest, RequestMemory])({target: emObservationListUrl}); + emoStore.get('').then(function(content) { + // Pull the EELs out of the rest content and create a new simple store from them. + var emos = content.observations; + + if (emos.length == 0) { + emoDiv = dom.byId('emo-grid'); + + if (emoDiv !== null) { + emoDiv.innerHTML = '<p> No EM observation entries so far. </p>'; + } + + // Let's try toggling the emo title pane closed. + //if (emo_tp.open) { emo_tp.toggle(); } + } else { + + // Notice that the +00:00 designating UTC will be stripped out since it + // is redundant. + var columns = [ + { field: 'created', label: 'Time Created (UTC)', get: function(object) { return object.created.replace('+00:00', '');} }, + { field: 'submitter', label: 'Submitter' }, + { field: 'group', label: 'MOU Group' }, + { field: 'footprint_count', label: 'N_regions' }, + { field: 'radec', + label: 'Covering (ra, dec)', + get: function(object){ + var raLoc = Math.round10(object.ra, -2); + var raHalfWidthLoc = Math.round10(object.raWidth/2.0, -2); + var decLoc = Math.round10(object.dec, -2); + var decHalfWidthLoc = Math.round10(object.decWidth/2.0, -2); + var rastring = raLoc + " \xB1 " + raHalfWidthLoc; + var decstring = decLoc + " \xB1 " + decHalfWidthLoc; + return "(" + rastring + ',' + decstring + ")"; + }, + } + ]; + + var subRowColumns = [ + { field: 'start_time', label: 'Start Time (UTC)', get: function(object) { return object.start_time.replace('+00:00', '');} }, + { field: 'exposure_time', label: 'Exposure Time (s)' }, + { field: 'ra', label: 'ra'}, + { field: 'raWidth', label: 'ra width'}, + { field: 'dec', label: 'dec'}, + { field: 'decWidth', label: 'dec width'} + ]; + + // Add extra class names to our grid cells so we can style them separately + for (i = 0; i < columns.length; i++) { + columns[i].className = 'supergrid-cell'; + } + for (i = 0; i < subRowColumns.length; i++) { + subRowColumns[i].className = 'subgrid-cell'; + } + + var grid = new Grid({ + columns: columns, + className: 'dgrid-autoheight', + + renderRow: function (object, options) { + // Add the supergrid-row class to the row so we can style it separately from the subrows. + var div = put('div.collapsed.supergrid-row', Grid.prototype.renderRow.call(this, object, options)); + + // Add the subdiv table which will expand and contract. + var t = put(div, 'div.expando table'); + // I'm finding that the table needs to be 100% of the available width, otherwise + // Firefox doesn't like it. Hence the extra empty column. + var subGridNode = put(t, 'tr td[style="width: 5%"]+td div'); + var sg = new Grid({ + columns: subRowColumns, + className: 'dgird-subgrid', + }, subGridNode); + sg.renderArray(object.footprints); + // Add the text comment div as long as the comment is not an empty string. + if (object.comment !== "") { + put(t, 'tr td[style="width: 5%"]+td div.subrid-text', object.comment); + } + + return div; + } + }, 'emo-grid'); + grid.renderArray(emos); + grid.set("sort", 'N', descending=true); + + var expandedNode = null; + + // listen for clicks to trigger expand/collapse in table view mode + var expandoListener = on(grid.domNode, '.dgrid-row:click', function (event) { + var node = grid.row(event).element; + var collapsed = node.className.indexOf('collapsed') >= 0; + + // toggle state of node which was clicked + put(node, (collapsed ? '!' : '.') + 'collapsed'); + + // XXX Commenting out the following two statements has the effect of allowing + // more than one of the subrows to be expanded at the same time. I think this + // is the sort of behavior that people expect. + // if clicked row wasn't expanded, collapse any previously-expanded row + // collapsed && expandedNode && put(expandedNode, '.collapsed'); + + // if the row clicked was previously expanded, nothing is expanded now + // expandedNode = collapsed ? node : null; + }); + } // endif on whether we have any emos or not. + }); + + + //------------------------------------------------------------------- + // Now that the annotations section has been added to the dom, we + // can work on its functionality. + //------------------------------------------------------------------- + var logtitle = dom.byId("logmessagetitle"); + var logtext = dom.byId("newlogtext"); + + var editor_div = dom.byId("editor"); + var preview_div = dom.byId("previewer"); + + // A pane holder for the form that will tag new log messages. + // I need it up here because we're going to integrate it with the + // editor components. + /* + dojo.style(preview_div, { 'display':'none'}); + dojo.style(editor_div, { 'display':'none'}); + + var button_element = dojo.create('button'); + dojo.place(button_element, logtitle, "right"); + var button = new Button({ + label: "Add Log Entry", + state: "add", + onClick: function(){ + if (this.state == 'add') { + dojo.style(editor_div, {'display':'block'}); + button.set('label', "Cancel Log Entry"); + button.set('state', 'cancel'); + editor.focus(); + } + else { + dojo.style(editor_div, {'display':'none'}); + dojo.style(preview_div, {'display':'none'}); + button.set('label', "Add Log Entry"); + button.set('state', 'add'); + editor.set('value',''); + } + }, + }, button_element); */ + + var savebutton = new Save({ + url: logSaveUrl, + onSuccess: function (resp, ioargs) { + //this.inherited(resp, ioargs); + this.button.set("disabled", false); + location.reload(true); + }, + onError: function (error, ioargs) { + //this.inherited(error, ioargs); + this.button.set("disabled", false); + alert(error); + }, + save: function(postdata) { + var newTagName = "analyst_comments"; + if (dom.byId('upload_input').files.length > 0) { + dom.byId("hidden_comment").value = postdata; + dom.byId("hidden_tagname").value = newTagName; + $("#file_attach_form").submit() + } else { + var postArgs = { + url: this.url, + content: { comment: postdata, tagname: newTagName }, + handleAs: "json" + }; + this.button.set("disabled", true); + var deferred = dojo.xhrPost(postArgs); + deferred.addCallback(dojo.hitch(this, this.onSuccess)); + deferred.addErrback(dojo.hitch(this, this.onError)); + } + + } + }); + + var previewbutton = new Preview({ + _preview: function(){ + var content = this.editor.get("value"); + preview_div.innerHTML = editor.get('value'); + dojo.style(preview_div, { + 'display':'block', + 'border': ".2em solid #900", + 'padding': '10px' + }); + MathJax.Hub.Queue(["Typeset",MathJax.Hub, preview_div]); + } + }); + + var editor = new Editor({ + extraPlugins : ['hiliteColor','|','createLink', + 'insertImage','fullscreen','viewsource','newpage', '|', previewbutton, savebutton] + }, editor_div); + editor.startup(); + + //------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------- + // The following section is for file attachments + // The idea of this form is to allow a user to attach a file to a log entry, but + // still submit the log entry via the usual save button. + var upload_div = dom.byId('upload_form_container'); + put(upload_div, 'p', 'Attach a file:') + var f = put(upload_div, 'form[action="' + logSaveUrl + '"]'); + put(f, '[method="post"]'); + put(f, '[enctype="multipart/form-data"]'); + put(f, '[id="file_attach_form"]'); + var i1 = put(f, 'input[id="upload_input"][name="upload"]'); + put(i1, '[multiple="false"]'); + put(i1, '[type="file"]'); + put(i1, '[label="Attach"]'); + put(i1, '[data-dojo-type="dojox.form.uploader"]'); + var i2 = put(f, 'input[id="hidden_comment"][name="comment"][type="hidden"]'); + var i3 = put(f, 'input[id="hidden_tagname"][name="tagname"][type="hidden"]'); + put(upload_div, 'br'); + + $("#emo_submit_form").submit(function(e) { + e.preventDefault(); + $.ajax({ + type: 'POST', + url: emObservationListUrl, + data: $(this).serialize(), + success: function(resp) { + //this.button.set("disabled", false); + location.reload(true); + }, + error: function(error) { + //this.button.set("disabled", false); + alert(error); + } + }); + }); + + $("#file_attach_form").submit(function(e) { + e.preventDefault(); + $.ajax({ + type: 'POST', + url: logSaveUrl, + data: new FormData(this), + enctype: 'multipart/form-data', + processData: false, + contentType: false, + cache: false, + success: function(resp) { + //this.button.set("disabled", false); + location.reload(true); + }, + error: function(error) { + //this.button.set("disabled", false); + alert(error); + } + }); + }); + //------------------------------------------------------------------------------------- + //------------------------------------------------------------------------------------- + + // For each log, attach callbacks for the tag delete and add buttons. + logs.forEach( function(log) { + // Attach a delete callback for each tag. + log.tag_names.forEach( function(tag_name) { + var del_button_id = "del_button_" + log.N + '_' + tag_name.replace(/ /g,"_"); + on(dom.byId(del_button_id), "click", getTagDelCallback(tag_name, log.N)); + new Tooltip({ connectId: del_button_id, label: "delete this tag" }); + }); + + // Attach an add tag callback for each log. + var add_button_id = 'addtag_' + log.N; + on(dom.byId(add_button_id), "click", getTagAddCallback(log.N)); + new Tooltip({ connectId: add_button_id, label: "tag this log message" }); + + }); + + var nodeList = query('.sV_button'); + for(var i=0; i<nodeList.length; i++){ + node = nodeList[i]; + var skymapName = node.id; + // Handle the post to skymapViewer button. + // Tacking on an invisible div with a form inside. + sVdiv = put(annotationsDiv, 'div#sV_form_div[style="display: none"]'); + sVform = put(sVdiv, 'form#sV_form[method="post"][action="$"]', + encodeURI(skymapViewerUrl)); + put(sVform, 'input[type="hidden"][name="skymapid"][value="{{ object.graceid }}"]'); + put(sVform, 'input[type="hidden"][name="json"]'); + put(sVform, 'input[type="hidden"][name="embb"]'); + put(sVform, 'input[type="submit"][value="View in skymapViewer!"]'); + + var sV_button = node; + if (sV_button) { + on(sV_button, "click", function(e) { + sjurl = fileDownloadUrl + '/' + e.target.id + '.json'; + var embblog_json; + var emobservation_json_url = emObservationListUrl; + + dojo.xhrGet({ + //url: embblog_json_url + "?format=json", + // Removing backwards compatibility hack + //url: emobservation_json_url + "?format=json&skymapViewer", + url: emobservation_json_url + "?format=json", + async: true, + load: function(embblog_json) { + + // fetch JSON content. + dojo.xhrGet({ + url: sjurl, + load: function(result) { + // Find the form and set its value to the appropriate JSON + sV_form = dom.byId("sV_form"); + // Shove the skymap.json contents into the value for the second form field. + sV_form.elements[1].value = result; + sV_form.elements[2].value = embblog_json; + // Submit the form, which takes the user to the skymapViewer server. + sV_form.submit(); + } + }); // end of inside ajax + } + }); // end of outside ajax + }); + } + } // end of loop over buttons + + + }); + }); + + +}); +