From 6ddc17a4ecece79fb5e0e524a7d5abc4e91f26f5 Mon Sep 17 00:00:00 2001
From: Tanner Prestegard <tanner.prestegard@ligo.org>
Date: Mon, 17 Sep 2018 12:15:05 -0500
Subject: [PATCH] Overhaul of LVAlert infrastructure

New format for LVAlerts (XMPP alerts).  New class-based
functionality for issuing alerts for events and superevents.
See https://git.ligo.org/lscsoft/gracedb/issues/8.
---
 gracedb/alerts/events/utils.py                | 110 +++++++---
 gracedb/alerts/main.py                        |  24 +-
 gracedb/alerts/superevents/utils.py           | 207 +++++-------------
 gracedb/alerts/xmpp.py                        |  39 ++--
 gracedb/api/v1/events/views.py                |  60 ++---
 gracedb/api/v1/superevents/serializers.py     |   2 +-
 .../api/v1/superevents/tests/test_access.py   |   6 +
 gracedb/api/v1/superevents/views.py           |   3 +-
 gracedb/events/view_logic.py                  |  37 ++--
 gracedb/events/view_utils.py                  |  31 ++-
 gracedb/events/views.py                       |  31 ++-
 gracedb/superevents/utils.py                  | 202 ++++++++++-------
 12 files changed, 371 insertions(+), 381 deletions(-)

diff --git a/gracedb/alerts/events/utils.py b/gracedb/alerts/events/utils.py
index 727ffa4bf..1a865b7fa 100644
--- a/gracedb/alerts/events/utils.py
+++ b/gracedb/alerts/events/utils.py
@@ -1,56 +1,100 @@
 from __future__ import absolute_import
 import logging
 
+from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
+from guardian.models import GroupObjectPermission
 
 from core.urls import build_absolute_uri
 from events.shortcuts import is_event
-from events.view_utils import eventToDict, eventLogToDict
+from events.view_utils import eventToDict, eventLogToDict, signoffToDict, \
+    emObservationToDict, embbEventLogToDict, groupeventpermissionToDict, \
+    labelToDict, voeventToDict
 from ..main import issue_alerts
+from ..utils import AlertIssuerWithParentObject
 
 # Set up logger
 logger = logging.getLogger(__name__)
 
 
-def event_alert_helper(obj, serializer=eventToDict, request=None):
-    """
-    If obj is not an event, assume it is an object with a relation to
-    an Event object.
-    """
+# NOTE: we have to be careful in all of these serializers since we want to
+# serialize the event subclass always, not the base event object.
+class AlertIssuerWithParentEvent(AlertIssuerWithParentObject):
+    parent_serializer_class = staticmethod(eventToDict)
 
-    # If superevent_id is None, assume obj is a Superevent
-    if is_event(obj):
-        graceid = event.graceid()
-    else:
-        try:
-            graceid = obj.event.graceid()
-        except:
-            # TODO: raise appropriate error
-            pass
+    def serialize_obj(self):
+        return self.serializer_class(self.obj)
 
-    # Construct URL for web view
-    url = build_absolute_uri(reverse('view', args=[graceid]), request)
+    def serialize_parent(self):
+        return self.parent_serializer_class(self.get_parent_obj(),
+            is_alert=True)
 
-    # Serialize the object into a dictionary
-    obj_dict = serializer(obj)
+    def _get_parent_obj(self):
+        # Assumes that the obj has a direct relation to an event
+        if not hasattr(self.obj, 'event'):
+            raise AttributeError(('object of class {0} does not have a direct '
+                'relationship to an event').format(
+                self.obj.__class__.__name__))
+        # Make sure we have the event "subclass"
+        return self.obj.event.get_subclass_or_self()
 
-    return url, obj_dict
+    def issue_alerts(self):
+        issue_alerts(self.get_parent_obj(), self.alert_type,
+           self.serialize_obj(), self.serialize_parent())
 
 
-def issue_alert_for_event_log(log, request=None):
+class EventAlertIssuer(AlertIssuerWithParentEvent):
+    serializer_class = staticmethod(eventToDict)
+    alert_types = ['new', 'update', 'selected_as_preferred',
+        'removed_as_preferred', 'added_to_superevent',
+        'removed_from_superevent']
 
-    # Get URL for event webview and serialized log
-    url, serialized_object = event_alert_helper(log, eventLogToDict, request)
+    def serialize_obj(self):
+        return self.serializer_class(self.obj.get_subclass_or_self(),
+            is_alert=True)
 
-    # Description
-    if log.filename:
-        description = "UPLOAD: '{filename}'".format(filename=log.filename)
-    else:
-        description = "LOG:"
-    description += " {message}".format(message=log.comment)
+    def _get_parent_obj(self):
+        return self.obj
 
-    # Send alerts
-    issue_alerts(log.event, alert_type="update", url=url,
-        description=description, serialized_object=serialized_object,
-        file_name=log.filename)
 
+class EventLogAlertIssuer(AlertIssuerWithParentEvent):
+    serializer_class = staticmethod(eventLogToDict)
+    alert_types = ['log']
+
+
+class EventLabelAlertIssuer(AlertIssuerWithParentEvent):
+    serializer_class = staticmethod(labelToDict)
+    alert_types = ['label_added', 'label_removed']
+
+
+class EventVOEventAlertIssuer(AlertIssuerWithParentEvent):
+    serializer_class = staticmethod(voeventToDict)
+    alert_types = ['voevent']
+
+
+class EventEMObservationAlertIssuer(AlertIssuerWithParentEvent):
+    serializer_class = staticmethod(emObservationToDict)
+    alert_types = ['emobservation']
+
+
+class EventEMBBEventLogAlertIssuer(AlertIssuerWithParentEvent):
+    serializer_class = staticmethod(embbEventLogToDict)
+    alert_types = ['embb_event_log']
+
+
+class EventSignoffAlertIssuer(AlertIssuerWithParentEvent):
+    serializer_class = staticmethod(signoffToDict)
+    alert_types = ['signoff_created', 'signoff_updated', 'signoff_deleted']
+
+
+class EventPermissionsAlertIssuer(EventAlertIssuer):
+    serializer_class = staticmethod(groupeventpermissionToDict)
+    alert_types = ['exposed', 'hidden']
+
+    def serialize_obj(self):
+        """self.obj should be an event here"""
+        gops = GroupObjectPermission.objects.filter(
+            object_pk=self.obj.pk,
+            content_type=ContentType.objects.get_for_model(self.obj))
+        gop_list = [self.serializer_class(gop) for gop in gops]
+        return gop_list
diff --git a/gracedb/alerts/main.py b/gracedb/alerts/main.py
index 03f36bac2..4c468ec2a 100644
--- a/gracedb/alerts/main.py
+++ b/gracedb/alerts/main.py
@@ -93,35 +93,29 @@ def get_alert_recips_for_label(event_or_superevent, label):
             .select_related('user')
         phone_recips |= trigger.contacts.exclude(phone="") \
             .select_related('user')
-        #email_recips.extend([c for c in
-        #    trigger.contacts.all().select_related('user') if c.email])
-        #phone_recips.extend([c for c in
-        #    trigger.contacts.all().select_related('user') if c.phone])
 
     return check_recips(email_recips), check_recips(phone_recips)
 
 
-def issue_alerts(event_or_superevent, alert_type, url=None, file_name="",
-    description="", label=None, serialized_object=None):
-
-    # Check alert_type
-    if alert_type not in ["new", "label", "update", "signoff"]:
-        raise ValueError(("alert_type is {0}, should be 'new', 'label', "
-            "'update', or 'signoff'").format(alert_type))
+def issue_alerts(event_or_superevent, alert_type, serialized_object,
+    serialized_parent=None):
 
     # Send XMPP alert
     if settings.SEND_XMPP_ALERTS:
-        issue_xmpp_alert(event_or_superevent, alert_type, file_name,
-            description=description, serialized_object=serialized_object)
+        issue_xmpp_alert(event_or_superevent, alert_type, serialized_object,
+            serialized_parent=serialized_parent)
 
     # Below here, we only do processing for email and phone alerts ------------
+    if not (settings.SEND_EMAIL_ALERTS or settings.SEND_PHONE_ALERTS):
+        return
 
     # TODO: make phone and e-mail alerts work for superevents
     if is_superevent(event_or_superevent):
         return
 
-    # We currently don't send phone or email alerts for updates or signoffs
-    if alert_type == "update" or alert_type == "signoff":
+    # Phone and email alerts are currently only for "new" and "label_added"
+    # alert_types, for events only.
+    if (alert_type not in ['new', 'label_added']):
         return
 
     # Don't send phone or email alerts for MDC events or Test events
diff --git a/gracedb/alerts/superevents/utils.py b/gracedb/alerts/superevents/utils.py
index b09eff780..b36931796 100644
--- a/gracedb/alerts/superevents/utils.py
+++ b/gracedb/alerts/superevents/utils.py
@@ -1,181 +1,78 @@
-from __future__ import absolute_import
-import logging
-
-from django.urls import reverse
-from django.contrib.auth.models import Group as AuthGroup
-
-from rest_framework.renderers import JSONRenderer
-
 from api.v1.superevents.serializers import SupereventSerializer, \
     SupereventLogSerializer, SupereventLabelSerializer, \
-    SupereventEMObservationSerializer, SupereventVOEventSerializer, \
+    SupereventVOEventSerializer, SupereventEMObservationSerializer, \
     SupereventSignoffSerializer, SupereventGroupObjectPermissionSerializer
-from core.urls import build_absolute_uri
-from superevents.shortcuts import is_superevent
 from ..main import issue_alerts
+from ..utils import AlertIssuerWithParentObject
 
-# Set up logger
-logger = logging.getLogger(__name__)
-
-
-def superevent_alert_helper(obj, serializer=SupereventSerializer,
-    request=None):
-    """
-    Assume non-superevent objects passed to this function have a
-    foreign key link to a superevent
-    """
-
-    # If superevent_id is None, assume obj is a Superevent
-    if is_superevent(obj):
-        superevent_id = obj.superevent_id
-    else:
-        try:
-            superevent_id = obj.superevent.superevent_id
-        except:
-            # TODO: raise appropriate error
-            pass
-
-    # Construct URL for web view
-    url = build_absolute_uri(reverse('superevents:view', args=[superevent_id]),
-        request)
-
-    # Serialize the object into a dictionary
-    obj_dict = serializer(obj).data
-
-    return url, obj_dict
-
-
-def issue_alert_for_superevent_creation(superevent, request=None):
-
-    # Get URL and serialized superevent
-    url, serialized_object = superevent_alert_helper(superevent,
-        request=request)
-
-    # Description
-    description = "NEW: superevent {0}".format(superevent.superevent_id)
-
-    # Send alerts
-    issue_alerts(superevent, alert_type="new", url=url,
-        description=description, serialized_object=serialized_object)
-
-
-#def issue_alert_for_superevent_update(superevent, request=None):
-#    # Get URL and serialized superevent
-#    url, serialized_object = superevent_alert_helper(superevent,
-#        request=request)
-#
-#    # Description
-#    # TODO: fix
-#    description = "UPDATE: superevent {0}".format(superevent.superevent_id)
-#
-#    # Send alerts
-#    issue_alerts(superevent, alert_type="update", url=url,
-#        description=description, serialized_object=serialized_object)
-
-
-def issue_alert_for_superevent_log(log, request=None):
-
-    # Get URL for superevent webview and serialized log
-    url, serialized_object = superevent_alert_helper(log,
-        SupereventLogSerializer, request=request)
-
-    # Description
-    if log.filename:
-        description = "UPLOAD: '{filename}'".format(filename=log.filename)
-    else:
-        description = "LOG:"
-    description += " {message}".format(message=log.comment)
-
-    # Send alerts
-    issue_alerts(log.superevent, alert_type="update", url=url,
-        description=description, serialized_object=serialized_object,
-        file_name=log.filename)
-
-
-def issue_alert_for_superevent_label_creation(labelling, request=None):
-
-    # Get URL for superevent webview and serialized label
-    url, serialized_object = superevent_alert_helper(labelling,
-        SupereventLabelSerializer, request=request)
-
-    # Description
-    description = "LABEL: {label} added".format(label=labelling.label.name)
-
-    # Send alerts
-    # NOTE: current alerts don't include an object (change this?)
-    issue_alerts(labelling.superevent, alert_type="label", url=url,
-        description=description, serialized_object=serialized_object)
 
+class AlertIssuerWithParentSuperevent(AlertIssuerWithParentObject):
+    parent_serializer_class = SupereventSerializer
 
-def issue_alert_for_superevent_label_removal(labelling, request=None):
-    # Get URL for superevent webview and serialized label
-    url, serialized_object = superevent_alert_helper(labelling,
-        SupereventLabelSerializer, request=request)
+    def serialize_parent(self):
+        return self.parent_serializer_class(self.get_parent_obj()).data
 
-    # Description
-    description = "UPDATE: {label} removed".format(label=labelling.label.name)
+    def _get_parent_obj(self):
+        # Assumes that the obj has a direct relation to a superevent
+        if not hasattr(self.obj, 'superevent'):
+            raise AttributeError(('object of class {0} does not have a direct '
+                'relationship to a superevent').format(
+                self.obj.__class__.__name__))
+        return self.obj.superevent
 
-    # Send alerts
-    issue_alerts(labelling.superevent, alert_type="update", url=url,
-        description=description, serialized_object=serialized_object)
+    def issue_alerts(self):
+        issue_alerts(self.get_parent_obj(), self.alert_type,
+            self.serialize_obj(), self.serialize_parent())
 
 
-def issue_alert_for_superevent_voevent(voevent, request=None):
-    # Get URL for superevent webview and serialized voevent
-    url, serialized_object = superevent_alert_helper(voevent,
-        SupereventVOEventSerializer, request=request)
+class SupereventAlertIssuer(AlertIssuerWithParentSuperevent):
+    serializer_class = SupereventSerializer
+    alert_types = ['new', 'update', 'event_added', 'event_removed',
+        'confirmed_as_gw']
 
-    # Description
-    description = "VOEVENT: {filename}".format(filename=voevent.filename)
+    def _get_parent_obj(self):
+        return self.obj
 
-    # Send alerts
-    issue_alerts(voevent.superevent, alert_type="update", url=url,
-        file_name=voevent.filename, description=description,
-        serialized_object=serialized_object)
 
+class SupereventLogAlertIssuer(AlertIssuerWithParentSuperevent):
+    serializer_class = SupereventLogSerializer
+    alert_types = ['log']
 
-def issue_alert_for_superevent_emobservation(emobservation, request=None):
-    # Get URL for superevent webview and serialized emo
-    url, serialized_object = superevent_alert_helper(emobservation,
-        SupereventEMObservationSerializer, request=request)
 
-    # Description
-    description = "New EMBB observation record for {group}".format(
-        group=emobservation.group.name)
+class SupereventLabelAlertIssuer(AlertIssuerWithParentSuperevent):
+    serializer_class = SupereventLabelSerializer
+    alert_types = ['label_added', 'label_removed']
 
-    # Send alerts
-    issue_alerts(emobservation.superevent, alert_type="update", url=url,
-        description=description, serialized_object=serialized_object)
 
+class SupereventVOEventAlertIssuer(AlertIssuerWithParentSuperevent):
+    serializer_class = SupereventVOEventSerializer
+    alert_types = ['voevent']
 
-def issue_alert_for_superevent_signoff(signoff, request=None):
-    # Get URL for superevent webview and serialized signoff
-    url, serialized_object = superevent_alert_helper(signoff,
-        SupereventSignoffSerializer, request=request)
 
-    # Description
-    description = signoff.status
+class SupereventEMObservationAlertIssuer(AlertIssuerWithParentSuperevent):
+    serializer_class = SupereventEMObservationSerializer
+    alert_types = ['emobservation']
 
-    # Send alerts
-    issue_alerts(signoff.superevent, alert_type="signoff", url=url,
-        description=description, serialized_object=serialized_object)
 
+class SupereventSignoffAlertIssuer(AlertIssuerWithParentSuperevent):
+    serializer_class = SupereventSignoffSerializer
+    alert_types = ['signoff_created', 'signoff_updated', 'signoff_deleted']
 
-def issue_alert_for_superevent_permissions(superevent, request=None):
-    # Construct URL for web view
-    url = build_absolute_uri(reverse('superevents:view', args=[
-        superevent.superevent_id]), request)
 
-    # Get serialized permissions
-    gops = superevent.supereventgroupobjectpermission_set.all()
-    gop_group_pks = gops.values_list('group', flat=True).distinct()
-    group_queryset = AuthGroup.objects.filter(pk__in=gop_group_pks)
-    serialized_list = SupereventGroupObjectPermissionSerializer(
-        group_queryset, many=True)
+class SupereventPermissionsAlertIssuer(AlertIssuerWithParentSuperevent):
+    serializer_class = SupereventGroupObjectPermissionSerializer
+    alert_types = ['exposed', 'hidden']
 
-    # Description
-    description = 'Permissions updated'
+    def serialize_obj(self):
+        """
+        self.obj should be a superevent, but we want to return the list
+        of group object permissions here.
+        """
+        # NOTE: it seems really weird to return a list of permissions here.
+        # But exposing/hiding a list of permissions does in fact change
+        # multiple permissions. Should give this some more thought.
+        gops = self.obj.supereventgroupobjectpermission_set.all()
+        return self.serializer_class(gops, many=True).data
 
-    # Send alerts
-    issue_alerts(superevent, alert_type="update", url=url,
-        description=description, serialized_object=serialized_list)
+    def _get_parent_obj(self):
+        return self.obj
diff --git a/gracedb/alerts/xmpp.py b/gracedb/alerts/xmpp.py
index 63b4f39d2..584787f12 100644
--- a/gracedb/alerts/xmpp.py
+++ b/gracedb/alerts/xmpp.py
@@ -63,8 +63,8 @@ def get_xmpp_node_names(event_or_superevent):
     return node_names
 
 
-def issue_xmpp_alert(event_or_superevent, alert_type="new", file_name="",
-    description="", serialized_object=None):
+def issue_xmpp_alert(event_or_superevent, alert_type, serialized_object,
+    serialized_parent=None):
     """
     serialized_object should be a dict
     """
@@ -76,33 +76,30 @@ def issue_xmpp_alert(event_or_superevent, alert_type="new", file_name="",
     # Determine LVAlert node names
     node_names = get_xmpp_node_names(event_or_superevent)
 
-    # Get object id
-    if is_superevent(event_or_superevent):
-        object_id = event_or_superevent.superevent_id
-    elif is_event(event_or_superevent):
-        object_id = event_or_superevent.graceid()
+    # Get uid - FIXME when graceid is switched to a property for events
+    # instead of a callable
+    if is_event(event_or_superevent):
+        uid = event_or_superevent.graceid()
     else:
-        error_msg = ('Object is of {0} type; should be an event '
-            'or superevent').format(type(event_or_superevent))
-        logger.error(error_msg)
-        raise TypeError(error_msg)
+        uid = event_or_superevent.graceid
 
     # Create the output dictionary and serialize as JSON.
     lva_data = {
-        'file': file_name,
-        'uid': object_id,
+        'uid': uid,
         'alert_type': alert_type,
-        'description': description,
-        'labels': [label.name for label in event_or_superevent.labels.all()]
+        'data': serialized_object,
     }
-    if serialized_object is not None:
-        lva_data['object'] = serialized_object
+    # Add serialized "parent" object
+    if serialized_parent is not None:
+        lva_data['object'] = serialized_parent
+
+    # Dump to JSON format:
     # simplejson.dumps is needed to properly handle Decimal fields
     msg = simplejson.dumps(lva_data)
 
     # Log message for debugging
-    logger.debug("issue_xmpp_alert: sending message {msg} for {object_id}" \
-        .format(msg=msg, object_id=object_id))
+    logger.info("issue_xmpp_alert: sending message {msg} for {uid}" \
+        .format(msg=msg, uid=uid))
 
     # Get manager ready for LVAlert Overseer (?)
     if settings.USE_LVALERT_OVERSEER:
@@ -118,8 +115,8 @@ def issue_xmpp_alert(event_or_superevent, alert_type="new", file_name="",
 
             # Log message
             logger.info(("issue_xmpp_alert: sending alert type {alert_type} "
-                "with message {msg_id} for {obj_id} to {node}").format(
-                alert_type=alert_type, msg_id=message_id, obj_id=object_id,
+                "with message {msg_id} for {uid} to {node}").format(
+                alert_type=alert_type, msg_id=message_id, uid=uid,
                 node=node_name))
 
             # Try to send with LVAlert Overseer (if enabled)
diff --git a/gracedb/api/v1/events/views.py b/gracedb/api/v1/events/views.py
index f5bcfd397..1a04d2dcb 100644
--- a/gracedb/api/v1/events/views.py
+++ b/gracedb/api/v1/events/views.py
@@ -32,7 +32,8 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, \
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
-from alerts.old_alert import issueAlertForUpdate
+from alerts.events.utils import EventAlertIssuer, EventLogAlertIssuer, \
+    EventVOEventAlertIssuer, EventPermissionsAlertIssuer
 from api.backends import LigoAuthentication
 from core.http import check_and_serve_file
 from core.vfile import VersionedFile
@@ -48,7 +49,8 @@ from events.view_logic import create_label, get_performance_info, \
     delete_label, _createEventFromForm, create_eel, create_emobservation
 from events.view_utils import eventToDict, eventLogToDict, labelToDict, \
     embbEventLogToDict, voeventToDict, emObservationToDict, signoffToDict, \
-    skymapViewerEMObservationToDict, BadFARRange, check_query_far_range
+    skymapViewerEMObservationToDict, BadFARRange, check_query_far_range, \
+    groupeventpermissionToDict
 from superevents.models import Superevent
 from .throttles import EventCreationThrottle, AnnotationThrottle
 from ...utils import api_reverse
@@ -609,6 +611,13 @@ class EventDetail(APIView):
             # of exceptions here.
             return Response("Bad Data",
                     status=status.HTTP_400_BAD_REQUEST)
+
+        # Save event
+        event.save()
+
+        # Issue alert
+        EventAlertIssuer(event, alert_type='update').issue_alerts()
+
         return Response(status=status.HTTP_202_ACCEPTED)
 
 #==================================================================
@@ -842,13 +851,7 @@ class EventLogList(APIView):
         #    response['tagWarning'] = tw_dict['tagWarning']
 
         # Issue alert.
-        description = "LOG: "
-        fname = ""
-        if uploadedFile:
-            description = "UPLOAD: '%s' " % uploadedFile.name
-            fname = uploadedFile.name
-        issueAlertForUpdate(event, description+message, doxmpp=True, 
-            filename=fname, serialized_object=rv)
+        EventLogAlertIssuer(logentry, alert_type='log').issue_alerts()
 
         return response
 
@@ -903,6 +906,7 @@ class EMBBEventLogList(APIView):
     @event_and_auth_required
     def post(self, request, event):
         try:
+            # Alert is issued in this code
             eel = create_eel(request.data, event, request.user)
         except ValueError, e:
             return Response("%s" % str(e), status=status.HTTP_400_BAD_REQUEST)
@@ -917,11 +921,6 @@ class EMBBEventLogList(APIView):
         response = Response(rv, status=status.HTTP_201_CREATED)
         response['Location'] = rv['self']
 
-        # Issue alert.
-        description = "New EMBB log entry."
-        issueAlertForUpdate(event, description, doxmpp=True,
-            filename="", serialized_object=rv)
-
         return response
 
 class EMBBEventLogDetail(APIView):
@@ -993,6 +992,7 @@ class EMObservationList(APIView):
     @event_and_auth_required
     def post(self, request, event):
         try:
+            # Create EMObservation - alert is issued inside this code
             emo = create_emobservation(request, event)
         except ValueError, e:
             return Response("%s" % str(e), status=status.HTTP_400_BAD_REQUEST)
@@ -1254,24 +1254,6 @@ class EventLogTagDetail(APIView):
 #==================================================================
 # Permission Resources
 
-def groupeventpermissionToDict(gop, event, request=None):
-    """Convert a group object permission to a dictionary.
-       Output depends on the level of specificity.
-    """
-
-    rv = {}
-    rv['group'] = gop.group.name
-    rv['graceid'] = event.graceid()
-    perm_shortname = gop.permission.codename.split('_')[0]
-    rv['permission'] = perm_shortname
-    # We want a link to the self only.  End of the line.
-    rv['links'] = {
-                    "self" : api_reverse("events:groupeventpermission-detail",
-                                     args=[event.graceid(),gop.group.name,perm_shortname],
-                                     request=request)
-                  }
-    return rv
-
 def getContentType(event):
     return ContentType.objects.get_for_model(event)
 
@@ -1398,6 +1380,9 @@ class GroupEventPermissionDetail(APIView):
         status_code = status.HTTP_200_OK
         if created:
             status_code = status.HTTP_201_CREATED
+            # Issue alert
+            EventPermissionsAlertIssuer(gop, alert_type='exposed') \
+                .issue_alerts()
         return Response(rv, status=status_code)
 
     #
@@ -1420,6 +1405,9 @@ class GroupEventPermissionDetail(APIView):
                 permission=permission)        
             gop.delete()
             event.refresh_perms()
+            # Issue alert
+            EventPermissionsAlertIssuer(gop, alert_type='hidden') \
+                .issue_alerts()
 
             # XXX if the event is a subclass, we need to delete perms on the
             # underlying event as well.
@@ -1575,9 +1563,7 @@ class Files(APIView):
                 pass
 
         try:
-            description = "UPLOAD: {0}".format(filename)
-            issueAlertForUpdate(event, description, doxmpp=True, 
-                filename=filename, serialized_object = eventLogToDict(logentry))
+            EventLogAlertIssuer(logentry, alert_type='log').issue_alerts()
         except:
             # XXX something should be done here.
             pass
@@ -1697,9 +1683,7 @@ class VOEventList(APIView):
             #rv['tagWarning'] = 'Error tagging VOEvent log message as em_follow.'
 
         # Issue alert.
-        description = "VOEVENT: %s" % filename
-        issueAlertForUpdate(event, description, doxmpp=True, 
-            filename=filename, serialized_object=rv)
+        EventVOEventAlertIssuer(voevent, alert_type='voevent').issue_alerts()
 
         response = Response(rv, status=status.HTTP_201_CREATED)
         response['Location'] = rv['links']['self']
diff --git a/gracedb/api/v1/superevents/serializers.py b/gracedb/api/v1/superevents/serializers.py
index 28a56ae6d..394d33066 100644
--- a/gracedb/api/v1/superevents/serializers.py
+++ b/gracedb/api/v1/superevents/serializers.py
@@ -243,7 +243,7 @@ class SupereventEventSerializer(serializers.ModelSerializer):
         submitter = validated_data.pop('user')
         add_event_to_superevent(superevent, event, submitter,
             add_superevent_log=True, add_event_log=True,
-            issue_superevent_alert=True, issue_event_alert=True)
+            issue_alert=True)
         return event
 
 
diff --git a/gracedb/api/v1/superevents/tests/test_access.py b/gracedb/api/v1/superevents/tests/test_access.py
index af959b433..950f9a42e 100644
--- a/gracedb/api/v1/superevents/tests/test_access.py
+++ b/gracedb/api/v1/superevents/tests/test_access.py
@@ -1351,6 +1351,8 @@ class TestSupereventLogList(AccessManagersGroupAndUserSetup,
 
     def test_lvem_user_create_log(self):
         """LV-EM user can create logs for exposed superevents only"""
+        # Create tag since external users' logs will be tagged with 'lvem'
+        Tag.objects.create(name=settings.EXTERNAL_ACCESS_TAGNAME)
         log_data = {'comment': 'test comment'}
 
         # Internal-only superevent
@@ -2238,6 +2240,10 @@ class TestSupereventEMObservationList(SupereventSetup, GraceDbApiTestBase):
 
     def test_lvem_user_create_emobservation_for_exposed_superevent(self):
         """LV-EM user can create EMObservations for exposed superevents"""
+        # Have to create lvem tag since there will be a log created to
+        # document the EMObservation creation and it will be tagged
+        # with 'lvem' since it was created by an external user.
+        Tag.objects.create(name=settings.EXTERNAL_ACCESS_TAGNAME)
         url = v_reverse('superevents:superevent-emobservation-list',
             args=[self.lvem_superevent.superevent_id])
         response = self.request_as_user(url, "POST", self.lvem_user,
diff --git a/gracedb/api/v1/superevents/views.py b/gracedb/api/v1/superevents/views.py
index 89253b50d..f7fe15830 100644
--- a/gracedb/api/v1/superevents/views.py
+++ b/gracedb/api/v1/superevents/views.py
@@ -141,8 +141,7 @@ class SupereventEventViewSet(mixins.ListModelMixin,
     def perform_destroy(self, instance):
         remove_event_from_superevent(instance.superevent, instance,
             self.request.user, add_superevent_log=True,
-            add_event_log=True, issue_superevent_alert=True,
-            issue_event_alert=True)
+            add_event_log=True, issue_alert=True)
 
 
 class SupereventLabelViewSet(viewsets.ModelViewSet,
diff --git a/gracedb/events/view_logic.py b/gracedb/events/view_logic.py
index 9b7c651c7..06db6ecfb 100644
--- a/gracedb/events/view_logic.py
+++ b/gracedb/events/view_logic.py
@@ -16,8 +16,8 @@ from .view_utils import eventToDict, eventLogToDict, emObservationToDict, \
     labelToDict
 from .permission_utils import assign_default_event_perms
 
-from alerts.old_alert import issueAlert, issueAlertForLabel, issueAlertForUpdate, \
-    issueXMPPAlert
+from alerts.events.utils import EventAlertIssuer, EventLabelAlertIssuer, \
+    EventEMObservationAlertIssuer, EventEMBBEventLogAlertIssuer
 from core.vfile import VersionedFile
 
 from django.contrib.contenttypes.models import ContentType
@@ -154,10 +154,7 @@ def _createEventFromForm(request, form):
                 # Send an alert.
                 # XXX This reverse will give the web-interface URL, not the REST URL.
                 # This could be a problem if anybody ever tries to use it.
-                issueAlert(event,
-                           request.build_absolute_uri(reverse("file-download", args=[event.graceid(),f.name])),
-                           request.build_absolute_uri(reverse("view", args=[event.graceid()])),
-                           eventToDict(event, request=request))
+                EventAlertIssuer(event, alert_type='new').issue_alerts()
             except Exception, e:
                 message = "Problem issuing an alert (%s)" % e
                 logger.warning(message)
@@ -215,12 +212,9 @@ def create_label(event, request, labelName, doAlert=True, doXMPP=True):
             logger.exception('Problem saving log message (%s)' % str(e))
             d['error'] = str(e)
 
-        # Serialize the labelling object
-        serialized_label = labelToDict(labelling)
-
         try:
-            issueAlertForLabel(event, label, doXMPP, event_url=event_url,
-                serialized_object=serialized_label)
+            EventLabelAlertIssuer(labelling, alert_type='label_added') \
+                .issue_alerts()
         except Exception as e:
             logger.exception('Problem issuing alert (%s)' % str(e))
             d['warning'] = "Problem issuing alert (%s)" % str(e)
@@ -263,15 +257,10 @@ def delete_label(event, request, labelName, doXMPP=True):
             logger.exception('Problem saving log message (%s)' % str(e))
             d['error'] = str(e)
 
-        # Serialize deleted labelling object
-        serialized_label = labelToDict(this_label)
-
         # send an XMPP alert, no email or phone alerts
         try:
-            if doXMPP:
-                issueXMPPAlert(event, "", alert_type="update",
-                    description="Label {0} removed".format(label.name),
-                    serialized_object=serialized_label)
+            EventLabelAlertIssuer(this_label, alert_type='label_removed') \
+                .issue_alerts()
         except Exception as e:
             logger.exception('Problem issuing alert (%s)' % str(e))
             d['warning'] = "Problem issuing alert (%s)" % str(e)
@@ -454,6 +443,11 @@ def create_eel(d, event, user):
 
     eel.validateMakeRects()
     eel.save()
+
+    # Issue alert
+    EventEMBBEventLogAlertIssuer(eel, alert_type='embb_event_log') \
+        .issue_alerts()
+
     return eel
 
 #
@@ -579,12 +573,11 @@ def create_emobservation(request, event):
     try:
         description = "New EMBB observation record for {group}".format(
             group=emo.group)
-        object = emObservationToDict(emo, request)
-        issueAlertForUpdate(event, description, doxmpp=True,
-            filename="", serialized_object=object)        
+        EventEMObservationAlertIssuer(emo, alert_type='emobservation') \
+            .issue_alerts()
     except Exception, e:
         # XXX Should probably send back warnings, as in the other cases.
-        pass
+        logger.error('error sending alert for emobservation: {0}'.format(e))
 
     # Write a log message
     log = EventLog.objects.create(issuer=user, comment=description, event=event)
diff --git a/gracedb/events/view_utils.py b/gracedb/events/view_utils.py
index e8d344de4..1db5ca57e 100644
--- a/gracedb/events/view_utils.py
+++ b/gracedb/events/view_utils.py
@@ -132,7 +132,7 @@ def reverse(name, *args, **kw):
 #---------------------------------------------------------------------------------------
 #---------------------------------------------------------------------------------------
 
-def eventToDict(event, columns=None, request=None):
+def eventToDict(event, columns=None, request=None, is_alert=False):
     """Convert an Event to a dictionary."""
     rv = {}
     graceid = event.graceid()
@@ -173,7 +173,10 @@ def eventToDict(event, columns=None, request=None):
     #      for labelling in event.labelling_set.all()])
     # XXX Try to produce a dictionary of analysis specific attributes.  Duck typing.
     # XXX These extra attributes should only be seen by internal users.
-    if request and request.user and not is_external(request.user): 
+    # So we only do this part if the user account is internal *OR* if this is
+    # for an LVAlert
+    if ((request and request.user and not is_external(request.user)) or
+        is_alert):
         rv['extra_attributes'] = {}
         try:
             # GrbEvent
@@ -629,6 +632,30 @@ def signoffToDict(signoff):
         'signoff_type': signoff.signoff_type,
     } 
 
+def groupeventpermissionToDict(gop, event=None, request=None):
+    """Convert a group object permission to a dictionary.
+       Output depends on the level of specificity.
+    """
+
+    # Hacky temporary measure
+    if event is None:
+        event = gop.content_object
+
+    rv = {}
+    rv['group'] = gop.group.name
+    rv['permission'] = gop.permission.codename
+    #rv['graceid'] = event.graceid()
+    #perm_shortname = gop.permission.codename.split('_')[0]
+    #rv['permission'] = perm_shortname
+    # We want a link to the self only.  End of the line.
+    #rv['links'] = {
+    #                "self" : api_reverse("events:groupeventpermission-detail",
+    #                                 args=[event.graceid(),gop.group.name,perm_shortname],
+    #                                 request=request)
+    #              }
+    return rv
+
+
 #---------------------------------------------------------------------------------------
 #---------------------------------------------------------------------------------------
 # Miscellany
diff --git a/gracedb/events/views.py b/gracedb/events/views.py
index 24e9e83a1..fc5254bd2 100644
--- a/gracedb/events/views.py
+++ b/gracedb/events/views.py
@@ -30,7 +30,9 @@ from .view_utils import flexigridResponse, jqgridResponse
 from .view_utils import get_recent_events_string
 from .view_utils import eventLogToDict
 from .view_utils import signoffToDict
-from alerts.old_alert import issueAlertForUpdate, issueXMPPAlert
+from alerts.events.utils import EventAlertIssuer, EventLogAlertIssuer, \
+    EventSignoffAlertIssuer, EventVOEventAlertIssuer, \
+    EventPermissionsAlertIssuer
 from superevents.models import Superevent
 
 # Set up logging
@@ -156,6 +158,9 @@ def voevent(request, event):
         # second argument should be a serial_number.
         voevent = buildVOEvent(event, voevent_type=voevent_type,
                                request=request, internal=internal)
+
+        # Issue alert
+        EventVOEventAlertIssuer(voevent, alert_type='voevent').issue_alerts()
     # Exceptions caused by user errors of some sort.
     except VOEventBuilderException, e:
         return HttpResponseBadRequest(str(e))
@@ -193,10 +198,12 @@ def _create(request):
 
         form = CreateEventForm(request.POST, request.FILES)
         if form.is_valid():
+            # Alert is issued in this function
             event, warnings = _createEventFromForm(request, form)
             if not event:
                 # problem creating event...  XXX need an error page for this.
                 raise Exception("\n".join(warnings))
+
             return HttpResponseRedirect(reverse(view, args=[event.graceid()]))
         else:
             rv['form'] = form
@@ -287,9 +294,7 @@ def logentry(request, event, num=None):
             else:
                 desc = "LOG: "
                 fname = ""
-            issueAlertForUpdate(event, desc+elog.comment, doxmpp=True,
-                filename=fname,
-                serialized_object=eventLogToDict(elog, request=request))
+            EventLogAlertIssuer(elog, alert_type='log').issue_alerts()
         except Exception as e:
             log.error('Error issuing alert: %s' % str(e))
             return HttpResponse("Failed to send alert for log message: %s" \
@@ -913,6 +918,9 @@ def update_event_perms_for_group(event, group, action):
         GroupObjectPermission.objects.get_or_create(
             content_type=ctype, group=group, permission=change,
             object_pk=event.id)
+
+        # Issue alert
+        EventPermissionsAlertIssuer(event, alert_type='exposed').issue_alerts()
     elif action=='protect':
         # Retrieve both group object permissions
         # Delete them
@@ -932,6 +940,8 @@ def update_event_perms_for_group(event, group, action):
         except GroupObjectPermission.DoesNotExist:
             # Couldn't find it. Take no action.
             pass
+        # Issue alert
+        EventPermissionsAlertIssuer(event, alert_type='hidden').issue_alerts()
 
     # lastly 
     event.refresh_perms()
@@ -985,6 +995,7 @@ def modify_permissions(request, event):
 def embblogentry(request, event, num=None):
     if request.method == "POST":
         try:
+            # Alert is issued inside this function
             create_eel(request.POST, event, request.user)
         except ValueError, e:
             return HttpResponseBadRequest(str(e))
@@ -1029,6 +1040,7 @@ def embblogentry(request, event, num=None):
 def emobservation_entry(request, event, num=None):
     if request.method == "POST":
         try:
+            # Alert is issued in this function
             create_emobservation(request, event)
         except ValueError, e:
             return HttpResponseBadRequest(str(e))
@@ -1172,8 +1184,8 @@ def modify_signoff(request, event):
             pass
 
         # Issue an alert.
-        issueXMPPAlert(event, location='', alert_type="signoff", description=status, 
-            serialized_object = signoffToDict(signoff))
+        EventSignoffAlertIssuer(signoff, alert_type='signoff_created') \
+            .issue_alerts()
 
     elif action=='edit':
         # get the existing object
@@ -1215,6 +1227,9 @@ def modify_signoff(request, event):
                 tag.event_logs.add(logentry)
             except:
                 pass
+            # Issue an alert.
+            EventSignoffAlertIssuer(signoff, alert_type='signoff_deleted') \
+                .issue_alerts()
         else:
             if status==None:
                 msg = "Please select a valid status."
@@ -1236,8 +1251,8 @@ def modify_signoff(request, event):
             signoff.comment = comment
             signoff.save()
             # Issue an alert.
-            issueXMPPAlert(event, location='', alert_type="signoff", description=status, 
-                serialized_object = signoffToDict(signoff))
+            EventSignoffAlertIssuer(signoff, alert_type='signoff_updated') \
+                .issue_alerts()
 
             # Create a log message
             msg = "updated %s signoff status as %s" % (signoff_type, status)
diff --git a/gracedb/superevents/utils.py b/gracedb/superevents/utils.py
index 26b6ffed6..f44e47c7b 100644
--- a/gracedb/superevents/utils.py
+++ b/gracedb/superevents/utils.py
@@ -10,19 +10,17 @@ from .buildVOEvent import construct_voevent_file
 from .models import Superevent, Log, Labelling, EMObservation, EMFootprint, \
     VOEvent, Signoff
 from .shortcuts import is_superevent
-from alerts.events.utils import issue_alert_for_event_log
-from alerts.superevents.utils import issue_alert_for_superevent_creation, \
-    issue_alert_for_superevent_log, \
-    issue_alert_for_superevent_label_creation, \
-    issue_alert_for_superevent_label_removal, \
-    issue_alert_for_superevent_emobservation, \
-    issue_alert_for_superevent_voevent, issue_alert_for_superevent_signoff, \
-    issue_alert_for_superevent_permissions
+from alerts.events.utils import EventAlertIssuer, EventLogAlertIssuer
+from alerts.superevents.utils import SupereventAlertIssuer, \
+    SupereventLogAlertIssuer, SupereventLabelAlertIssuer, \
+    SupereventVOEventAlertIssuer, SupereventEMObservationAlertIssuer, \
+    SupereventSignoffAlertIssuer, SupereventPermissionsAlertIssuer
 from core.permission_utils import expose_log_to_lvem, expose_log_to_public, \
     hide_log_from_lvem, hide_log_from_public, assign_perms_to_obj, \
     remove_perms_from_obj
 from core.vfile import create_versioned_file
 from events.models import Event, EventLog, Tag, Label
+from events.permission_utils import is_external
 from events.shortcuts import is_event
 
 # Set up logger
@@ -40,8 +38,6 @@ SUPEREVENT_PERMS = {
 }
 
 
-# TODO:
-# Add decorator to check access permissions (??) not sure if we should do it here or in the viewset itself
 def create_superevent(submitter, t_start, t_0, t_end, preferred_event,
     events=[], labels=[], category='P', add_log_message=True,
     issue_alert=True):
@@ -90,37 +86,36 @@ def create_superevent(submitter, t_start, t_0, t_end, preferred_event,
     # Superevent log message and alerts are taken care of elsewhere, but we
     # want to record logs for the individual events
     # NOTE: we don't have to worry about a repeat here for the preferred event
-    # since events comes directly from the serializer and hasn't been updated
-    # to include the preferred event (like it would be if we accessed
-    # s.events.all())
+    # since the 'events' list comes directly from the serializer and hasn't
+    # been updated to include the preferred event (like it would be if we
+    # accessed s.events.all() directly)
     # Alerts aren't issued here since we want to do that *after* the superevent
     # creation alert is issued below.
-    event_log_list = []
     for event in events:
-        _, el = add_event_to_superevent(s, event, submitter,
+        add_event_to_superevent(s, event, submitter,
             add_superevent_log=False, add_event_log=True,
-            issue_superevent_alert=False, issue_event_alert=False)
-        if el is not None:
-            event_log_list.append(el)
+            issue_alert=False)
 
     # Issue all relevant alerts
     if issue_alert:
         # Send "new" alert about superevent creation
-        issue_alert_for_superevent_creation(s)
+        SupereventAlertIssuer(s, alert_type='new').issue_alerts()
 
-        # "Manually" issue alerts for preferred_event and events
-        issue_alert_for_event_log(pref_event_log)
+        # "Manually" issue alerts for preferred_event
+        EventAlertIssuer(preferred_event, alert_type='selected_as_preferred') \
+            .issue_alerts()
 
-        for el in event_log_list:
-            issue_alert_for_event_log(el)
+        # "Manually" issue alerts for events
+        for event in events:
+            EventAlertIssuer(event, alert_type='added_to_superevent') \
+                .issue_alerts()
 
     # Add labels
     for label in labels:
         l = add_label_to_superevent(s, label, submitter,
             add_log_message=True, issue_alert=issue_alert)
 
-    # Look at event creation functions to see if there is anything else we should add here.
-    # CREATE DIRECTORY
+    # Create superevent data directory
     os.makedirs(s.datadir)
 
     return s
@@ -154,26 +149,37 @@ def update_superevent(superevent, updater, add_log_message=True,
         update_comment = "Updated superevent parameters: {0}".format(
             ", ".join(updates))
         update_log = create_log(updater, update_comment, superevent,
-            issue_alert=issue_alert)
+            issue_alert=False)
 
-        # Write event log messages if preferred event changed
+        # If preferred event changed, do a few things
         if new_params.has_key('preferred_event') and \
             (old_params['preferred_event'] != new_params['preferred_event']):
-            # Old preferred event
+            # Write log for old preferred event
             old_msg = ("Removed as preferred event for superevent: "
                 "{superevent_id}").format(superevent_id=
                 superevent.superevent_id)
             old_log = create_log(updater, old_msg,
-                    old_params['preferred_event'], issue_alert=issue_alert)
+                    old_params['preferred_event'], issue_alert=False)
 
-            # New preferred event
+            # Write log for new preferred event
             new_msg = ("Set as preferred event for superevent: "
                 "{superevent_id}").format(superevent_id=
                 superevent.superevent_id)
             new_log = create_log(updater, new_msg,
-                new_params['preferred_event'], issue_alert=issue_alert)
-
-    # TODO: issue alert separately from log creation
+                new_params['preferred_event'], issue_alert=False)
+
+            # Issue alerts for both
+            if issue_alert:
+                # Old
+                EventAlertIssuer(old_params['preferred_event'],
+                    alert_type='removed_as_preferred').issue_alerts()
+                # New
+                EventAlertIssuer(new_params['preferred_event'],
+                    alert_type='selected_as_preferred').issue_alerts()
+
+    # Superevent alerts
+    if issue_alert:
+        SupereventAlertIssuer(superevent, alert_type='update').issue_alerts()
 
     return superevent
 
@@ -198,19 +204,21 @@ def create_log(issuer, comment, event_or_superevent, filename="",
 
     if is_superevent(event_or_superevent):
         log_dict['superevent'] = event_or_superevent
-        LogModel = Log
-        alert_func = issue_alert_for_superevent_log
+        log_attr = 'log_set'
+        alerter_class = SupereventLogAlertIssuer
     elif is_event(event_or_superevent):
         log_dict['event'] = event_or_superevent
-        LogModel = EventLog
-        alert_func = issue_alert_for_event_log
+        log_attr = 'eventlog_set'
+        alerter_class = EventLogAlertIssuer
     else:
-        # TODO: raise error
-        logger.error(type(event_or_superevent))
-        pass
+        err_msg = "object is of type '{0}'; should be event or superevent" \
+            .format(type(event_or_superevent))
+        logger.error(err_msg)
+        raise TypeError(err_msg)
 
     # Create log object
-    log = LogModel.objects.create(**log_dict)
+    log_set = getattr(event_or_superevent, log_attr)
+    log = log_set.create(**log_dict)
 
     # Create versioned file
     if data_file:
@@ -226,26 +234,36 @@ def create_log(issuer, comment, event_or_superevent, filename="",
         add_tag_to_log(log, t, issuer, issue_alert=False)
 
     if issue_alert:
-        alert_func(log)
+        alerter_class(log, alert_type='log').issue_alerts()
 
-    # TODO:
-    # If user is external, add LV-EM tagname to this log message
+    # If user is external, add LV-EM tagname to this log message and expose it
+    # TODO: should it be exposed to the public? Or to LV-EM only?  We will
+    # stick with LV-EM only for now.
+    if is_external(issuer) and not autogenerated:
+        lvem_tag = Tag.objects.get(name=settings.EXTERNAL_ACCESS_TAGNAME)
+        add_tag_to_log(log, lvem_tag, issuer, add_log_message=True)
 
     return log 
 
 
 def get_log_parent(log):
-    # Determine if this is an event or superevent log
+    """Utility function to determine if this is an event or superevent log"""
+
     if isinstance(log, Log):
         return log.superevent
     elif isinstance(log, EventLog):
         return log.event
     else:
-        # TODO: raise exception
-        pass
+        err_msg = ("object is of type '{0}'; should be superevent log or "
+            "event log").format(type(log))
+        logger.error(err_msg)
+        raise TypeError(err_msg)
 
 
 def add_tag_to_log(log, tag, user, add_log_message=True, issue_alert=False):
+    # Presently, we don't issue alerts for tag addition or for the logs
+    # that are generated as a result.
+
     # Add tag to log
     log.tags.add(tag)
 
@@ -271,13 +289,15 @@ def add_tag_to_log(log, tag, user, add_log_message=True, issue_alert=False):
             tag_name=tag.name)
         event_or_superevent = get_log_parent(log)
         log_for_tag_addition = create_log(user, comment, event_or_superevent,
-            issue_alert=issue_alert, autogenerated=True)
+            issue_alert=False, autogenerated=True)
 
     return log_for_tag_addition
 
 
 def remove_tag_from_log(log, tag, user, add_log_message=True,
     issue_alert=False):
+    # Presently, we don't issue alerts for tag addition or for the logs
+    # that are generated as a result.
 
     # Remove tag from log
     log.tags.remove(tag)
@@ -304,17 +324,14 @@ def remove_tag_from_log(log, tag, user, add_log_message=True,
             N=log.N, tag_name=tag.name)
         event_or_superevent = get_log_parent(log)
         log_for_tag_removal = create_log(user, comment, event_or_superevent,
-            issue_alert=issue_alert, autogenerated=True)
+            issue_alert=False, autogenerated=True)
+
 
     return log_for_tag_removal
 
 
 def add_event_to_superevent(superevent, event, user, add_event_log=True,
-    add_superevent_log=True, issue_event_alert=True,
-    issue_superevent_alert=True):
-    """
-    We return log objects in case they are needed elsewhere
-    """
+    add_superevent_log=True, issue_alert=True):
 
     # Check that the event is of the correct type to be added
     # to a superevent
@@ -330,31 +347,32 @@ def add_event_to_superevent(superevent, event, user, add_event_log=True,
     superevent.events.add(event)
 
     # Create superevent log message to record event addtion?
-    superevent_log_for_event_addition = None
     if add_superevent_log:
         # Record event addition in superevent logs
         superevent_comment = 'Added event: {graceid}'.format(
             graceid=event.graceid())
         superevent_log_for_event_addition = create_log(user,
-            superevent_comment, superevent, issue_alert=issue_superevent_alert,
+            superevent_comment, superevent, issue_alert=False,
             autogenerated=True)
 
     # Create event log message to record addition to superevent?
-    event_log_for_addition_to_superevent = None
     if add_event_log:
         # Record addition to superevent in event logs
         event_comment = 'Added to superevent: {superevent_id}'.format(
             superevent_id=superevent.superevent_id)
         event_log_for_addition_to_superevent = create_log(user, event_comment,
-            event, issue_alert=issue_event_alert, autogenerated=True)
+            event, issue_alert=False, autogenerated=True)
 
-    return superevent_log_for_event_addition, \
-        event_log_for_addition_to_superevent
+    # Issue alerts
+    if issue_alert:
+        SupereventAlertIssuer(superevent, alert_type='event_added') \
+            .issue_alerts()
+        EventAlertIssuer(event, alert_type='added_to_superevent') \
+            .issue_alerts()
 
 
 def remove_event_from_superevent(superevent, event, user, add_event_log=True,
-    add_superevent_log=True, issue_event_alert=True,
-    issue_superevent_alert=True):
+    add_superevent_log=True, issue_alert=True):
     """
     This function should be within a try-except block to catch exceptions and
     convert them to the appropriate response.
@@ -374,18 +392,21 @@ def remove_event_from_superevent(superevent, event, user, add_event_log=True,
         superevent_comment = 'Removed event: {graceid}'.format(
             graceid=event.graceid())
         superevent_log_for_event_removal = create_log(user, superevent_comment,
-            superevent, issue_alert=issue_superevent_alert, autogenerated=True)
+            superevent, issue_alert=False, autogenerated=True)
 
     # Create event log message to record removal from superevent?
-    event_log_for_removal_from_superevent = None
     if add_event_log:
         event_comment ='Removed from superevent: {superevent_id}'.format(
             superevent_id=superevent.superevent_id)
         event_log_for_removal_from_superevent = create_log(user, event_comment,
-            event, issue_alert=issue_event_alert, autogenerated=True)
+            event, issue_alert=False, autogenerated=True)
 
-    return superevent_log_for_event_removal, \
-        event_log_for_removal_from_superevent
+    # Issue alerts
+    if issue_alert:
+        SupereventAlertIssuer(superevent, alert_type='event_removed') \
+            .issue_alerts()
+        EventAlertIssuer(event, alert_type='removed_from_superevent') \
+            .issue_alerts()
 
 
 def add_label_to_superevent(superevent, label, user, add_log_message=True,
@@ -403,7 +424,8 @@ def add_label_to_superevent(superevent, label, user, add_log_message=True,
             issue_alert=False, autogenerated=True)
 
     if issue_alert:
-        issue_alert_for_superevent_label_creation(labelling)
+        SupereventLabelAlertIssuer(labelling, alert_type='label_added') \
+            .issue_alerts()
 
     return labelling, log_for_label_addition
 
@@ -425,10 +447,12 @@ def remove_label_from_superevent(labelling, user, add_log_message=True,
     # labelling object still exists in memory, even though it has been
     # removed from the database and does not have an ID anymore
     if issue_alert:
-        issue_alert_for_superevent_label_removal(labelling)
+        SupereventLabelAlertIssuer(labelling, alert_type='label_removed') \
+            .issue_alerts()
 
     return log_for_label_removal
 
+
 def get_or_create_tag(tag_name, display_name=None):
 
     tag, created = Tag.objects.get_or_create(name=tag_name)
@@ -441,9 +465,11 @@ def get_or_create_tag(tag_name, display_name=None):
 
 def get_or_create_tags(tag_name_list, display_name_list=[]):
 
-    # TODO: make this a useful error
+    # Check that lists are the same length if display_name_list is
+    # provided
     if display_name_list and (len(display_name_list) != len(tag_name_list)):
-        raise ValueError('')
+        raise ValueError('If a list of display names is provided, it must '
+            'have the same length as the list of tag names')
 
     tag_list = []
     for i, tag_name in enumerate(tag_name_list):
@@ -491,9 +517,14 @@ def confirm_superevent_as_gw(superevent, user, add_log_message=True,
     if add_log_message:
         message = ("Confirmed as a gravitational wave: ID changed from "
             "{old} -> {new}").format(old=old_id, new=superevent.superevent_id)
-        gw_log = create_log(user, message, superevent, issue_alert=issue_alert,
+        gw_log = create_log(user, message, superevent, issue_alert=False,
             autogenerated=False)
 
+    # Issue alert
+    if issue_alert:
+        SupereventAlertIssuer(superevent, alert_type='confirmed_as_gw') \
+            .issue_alerts()
+
     return gw_log
 
 
@@ -525,7 +556,8 @@ def create_emobservation_for_superevent(superevent, submitter, ra_list,
 
     # Issue alert
     if issue_alert:
-        issue_alert_for_superevent_emobservation(emo)
+        SupereventEMObservationAlertIssuer(emo, alert_type='emobservation') \
+            .issue_alerts()
 
     return emo
 
@@ -573,12 +605,12 @@ def create_voevent_for_superevent(superevent, issuer, voevent_type,
 
     # Issue an alert
     if issue_alert:
-        issue_alert_for_superevent_voevent(voevent)
+        SupereventVOEventAlertIssuer(voevent, alert_type='voevent') \
+            .issue_alerts()
 
     return voevent
 
 
-# TODO: wrap this function in a try-except block in the form
 def create_signoff(superevent, user, signoff_type, signoff_instrument,
     signoff_status, signoff_comment, add_log_message=True, issue_alert=True):
 
@@ -611,7 +643,8 @@ def create_signoff(superevent, user, signoff_type, signoff_instrument,
 
     # Issue alert
     if issue_alert:
-        issue_alert_for_superevent_signoff(signoff)
+        SupereventSignoffAlertIssuer(signoff, alert_type='signoff_created') \
+            .issue_alerts()
 
     return signoff
 
@@ -626,8 +659,6 @@ def update_signoff(signoff, user, status, comment, add_log_message=True,
     updated_attributes = {k: v for k,v in new_data.items()
         if getattr(signoff, k, None) != v}
     old_data = {k: getattr(signoff, k) for k in updated_attributes}
-    logger.debug(updated_attributes)
-    logger.debug(old_data)
 
     # Get superevent
     superevent = signoff.superevent
@@ -701,9 +732,10 @@ def update_signoff(signoff, user, status, comment, add_log_message=True,
         signoff_log = create_log(user, full_comment, superevent,
             issue_alert=False, tags=[em_follow])
 
-    # Issue alert #TODO: make this specifically for a signoff update
+    # Issue alert
     if issue_alert:
-        issue_alert_for_superevent_signoff(signoff)
+        SupereventSignoffAlertIssuer(signoff, alert_type='signoff_updated') \
+            .issue_alerts()
 
     return signoff
 
@@ -747,7 +779,8 @@ def delete_signoff(signoff, user, add_log_message=True,
 
     # Alert
     if issue_alert:
-        issue_alert_for_superevent_log(signoff_log)
+        SupereventSignoffAlertIssuer(signoff, alert_type='signoff_deleted') \
+            .issue_alerts()
 
 
 def expose_superevent(superevent, user, add_log_message=True,
@@ -775,7 +808,8 @@ def expose_superevent(superevent, user, add_log_message=True,
 
     # Send alert
     if issue_alert:
-        issue_alert_for_superevent_permissions(superevent)
+        SupereventPermissionsAlertIssuer(superevent, alert_type='exposed') \
+            .issue_alerts()
 
 
 def hide_superevent(superevent, user, add_log_message=True,
@@ -803,5 +837,5 @@ def hide_superevent(superevent, user, add_log_message=True,
 
     # Send alert
     if issue_alert:
-        issue_alert_for_superevent_permissions(superevent)
-
+        SupereventPermissionsAlertIssuer(superevent, alert_type='hidden') \
+            .issue_alerts()
-- 
GitLab