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 %} &lt; {% endif %}{{ display_far|scientific }}</td>
+        <td>{% if far_is_upper_limit %} &lt; {% 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 = '&times;';
+                                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 = '&#43;';
+
+                            // 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 = '&times;';
+                                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 = '&#43;';
+
+                            // 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
+
+
+        });
+    });
+
+
+});
+