Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
utils.py 25.88 KiB
from django.shortcuts import get_object_or_404
from django.http import Http404

from .buildVOEvent import construct_voevent_file
from .models import Superevent, Log, Labelling, EMObservation, EMFootprint, \
    VOEvent, Signoff
from .shortcuts import is_superevent
from events.models import Event, EventLog, Tag, Label
from events.permission_utils import assign_default_perms
from events.shortcuts import is_event
from core.vfile import create_versioned_file
from alerts.superevent_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
from alerts.event_utils import issue_alert_for_event_log

import os
from core.permission_utils import expose_log_to_lvem, expose_log_to_public, \
    hide_log_from_lvem, hide_log_from_public

import logging
logger = logging.getLogger(__name__)

# NOTE: everything in here assumes that permissions have already been checked
# and handled properly

# 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):
    """
    Utility method for creating superevents.

    Arguments:
        submitter: User object (required)
        t_start, t_0, t_end: floats (required)
        preferred_event: Event object (required)
        events: list or QuerySet of Event objects
        labels: list of QuerySet of Label objects

    Usage:
        create_superevent(
            UserModel.objects.get(username='albert.einstein@ligo.org'),
            t_start=1, t_0=2, t_end=3, preferred_event=
            Event.objects.get(id='123400')
        )
    """

    # Create superevent
    s = Superevent.objects.create(submitter=submitter, t_start=t_start,
        t_0=t_0, t_end=t_end, preferred_event=preferred_event,
        category=category)

    # Create a log message to record initial superevent parameters
    creation_comment = ("Superevent created with t_start={t_start}, t_0={t_0},"
        " t_end={t_end}, preferred_event={preferred_event}").format(
        t_start=t_start, t_0=t_0, t_end=t_end, preferred_event=preferred_event)
    if events:
        creation_comment += ", events={events}".format(events=", ".join(
            [ev.graceid() for ev in events]))
    # This is autogenerated, but people probably don't want to hide it
    # in the web interface
    superevent_creation_log = create_log(submitter, creation_comment, s,
        issue_alert=False, autogenerated=False)

    # Write a log message for preferred event
    pref_event_msg = "Set as preferred event for superevent: {superevent_id}" \
        .format(superevent_id=s.superevent_id)
    pref_event_log = create_log(submitter, pref_event_msg, preferred_event,
        issue_alert=False, autogenerated=False)

    # Add events to superevent
    # 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())
    # 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_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 all relevant alerts
    if issue_alert:
        # Send "new" alert about superevent creation
        issue_alert_for_superevent_creation(s)

        # "Manually" issue alerts for preferred_event and events
        issue_alert_for_event_log(pref_event_log)

        for el in event_log_list:
            issue_alert_for_event_log(el)

    # Add labels
    for label in labels:
        l = add_label_to_superevent(s, label, submitter,
            add_log_message=True, issue_alert=issue_alert)

    # Create default GroupObjectPermissions - LVC group and executives group
    # can view/change superevents
    assign_default_perms(s)

    # Look at event creation functions to see if there is anything else we should add here.
    # CREATE DIRECTORY
    os.makedirs(s.datadir)

    return s


def update_superevent(superevent, updater, issue_alert=True, **kwargs):
    """
    kwargs which are used as superevent parameters:
        t_start, t_0, t_end, preferred_event
    """

    # Extract "updatable" superevent params from kwargs
    param_names = ['t_start', 't_0', 't_end', 'preferred_event']
    new_params = {k: v for k,v in kwargs.iteritems() if k in param_names}

    # Get old parameters
    old_params = {k: getattr(superevent, k) for k in new_params.keys()}

    # Update superevent object
    for k,v in new_params.iteritems():
        setattr(superevent, k, v)
    superevent.save()

    # Write a log message
    # 'if' statement to handle patch requests which don't change entire object
    updates = ["{name}: {old} -> {new}".format(name=k, old=old_params[k],
        new=new_params[k]) for k in new_params.keys()
        if old_params[k] != new_params[k]]
    update_comment = "Updated superevent parameters: {0}".format(
        ", ".join(updates))
    update_log = create_log(updater, update_comment, superevent,
        issue_alert=issue_alert)

    # Write event log messages if preferred event changed
    if new_params.has_key('preferred_event') and \
        (old_params['preferred_event'] != new_params['preferred_event']):
        # 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)

        # 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)

    return superevent


def create_log(issuer, comment, event_or_superevent, filename="",
    data_file=None, tags=[], file_version=None, issue_alert=False,
    autogenerated=False):
    """
    Assume data have already been validated by a form or serializer.
    tags should be a list of Tag objects (??)
    data_file should be an in-memory file type
    """

    # Initial dictionary for log creation
    log_dict = {
        'issuer': issuer,
        'comment': comment,
        'filename': filename,
    }
    if file_version is not None:
        log_dict['file_version'] = file_version

    if is_superevent(event_or_superevent):
        log_dict['superevent'] = event_or_superevent
        LogModel = Log
        alert_func = issue_alert_for_superevent_log
    elif is_event(event_or_superevent):
        log_dict['event'] = event_or_superevent
        LogModel = EventLog
        alert_func = issue_alert_for_event_log
    else:
        # TODO: raise error
        logger.error(type(event_or_superevent))
        pass

    # Create log object
    log = LogModel.objects.create(**log_dict)

    # Create versioned file
    if data_file:
        version = create_versioned_file(filename, event_or_superevent.datadir,
            data_file)

        # Update file_version
        log.file_version = version
        log.save(update_fields=['file_version'])

    # Add tags to log messages
    for t in tags:
        add_tag_to_log(log, t, issuer, issue_alert=False)

    if issue_alert:
        alert_func(log)

    # TODO:
    # If user is external, add LV-EM tagname to this log message

    return log 


def get_log_parent(log):
    # 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


def add_tag_to_log(log, tag, user, add_log_message=True, issue_alert=False):
    # Add tag to log
    log.tags.add(tag)

    # If this tag controls whether the log is exposed or not, we need to create
    # a corresponding GroupObjectPermission.  If we get to this point,
    # permissions should have already been checked.
    if (tag.name == settings.EXTERNAL_ACCESS_TAGNAME):
        expose_log_to_lvem(log)
    elif (tag.name == settings.PUBLIC_ACCESS_TAGNAME):
        expose_log_to_public(log)
        # Publicly exposed tags should also be exposed to LV-EM, if they
        # aren't already
        lvem_tag_applied = log.tags.filter(
            name=settings.EXTERNAL_ACCESS_TAGNAME).exists()
        if not lvem_tag_applied:
            lvem_tag = Tag.objects.get(name=settings.EXTERNAL_ACCESS_TAGNAME)
            add_tag_to_log(log, lvem_tag, user)

    # Create log message to record tag addition?
    log_for_tag_addition = None
    if add_log_message:
        comment = 'Tagged message {N}: {tag_name}.'.format(N=log.N,
            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)

    return log_for_tag_addition


def remove_tag_from_log(log, tag, user, add_log_message=True,
    issue_alert=False):

    # Remove tag from log
    log.tags.remove(tag)

    # If this tag controls whether the log is exposed or not, we need to create
    # a corresponding GroupObjectPermission.  If we get to this point,
    # permissions should have already been checked.
    if (tag.name == settings.EXTERNAL_ACCESS_TAGNAME):
        hide_log_from_lvem(log)
        # If the log is hidden from LV-EM, it should also be hidden from the
        # public
        public_tag_applied = log.tags.filter(
            name=settings.PUBLIC_ACCESS_TAGNAME).exists()
        if not public_tag_applied:
            public_tag = Tag.objects.get(name=settings.PUBLIC_ACCESS_TAGNAME)
            remove_tag_from_log(log, public_tag, user)
    elif (tag.name == settings.PUBLIC_ACCESS_TAGNAME):
        hide_log_from_public(log)

    # Create log message to record tag removal?
    log_for_tag_removal = None
    if add_log_message:
        comment = 'Removed tag {tag_name} from message {N}.'.format(
            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)

    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
    """

    # Check that the event is of the correct type to be added
    # to a superevent
    if not superevent.event_compatible(event):
        raise Superevent.EventCategoryMismatchError(
            _(('Event {graceid} is of type \'{e_category}\', and '
               'cannot be assigned to a superevent of type '
               '\'{s_category}\'').format(graceid=event.graceid(),
                e_category=event.get_event_category(),
                s_category=superevent.get_category_display())))

    # Add event to superevent
    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,
            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)

    return superevent_log_for_event_addition, \
        event_log_for_addition_to_superevent


def remove_event_from_superevent(superevent, event, user, add_event_log=True,
    add_superevent_log=True, issue_event_alert=True,
    issue_superevent_alert=True):
    """
    This function should be within a try-except block to catch exceptions and
    convert them to the appropriate response.
    """
    # Throw error if this is the preferred event
    if event == superevent.preferred_event:
        raise Superevent.PreferredEventRemovalError("Can't remove a "
            "superevent's preferred event without setting a new one.")

    # Remove event from superevent
    superevent.events.remove(event)

    # Create superevent log message to record event removal?
    superevent_log_for_event_removal = None
    if add_superevent_log:
        # Record event removal in superevent logs
        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)

    # 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)

    return superevent_log_for_event_removal, \
        event_log_for_removal_from_superevent


def add_label_to_superevent(superevent, label, user, add_log_message=True,
    issue_alert=True):

    # Create Labelling object
    labelling = Labelling.objects.create(label=label, creator=user,
        superevent=superevent)

    log_for_label_addition = None
    if add_log_message:
        # Record label addition in superevent logs
        comment = 'Added label: {label_name}'.format(label_name=label.name)
        log_for_label_addition = create_log(user, comment, superevent,
            issue_alert=False, autogenerated=True)

    if issue_alert:
        issue_alert_for_superevent_label_creation(labelling)

    return labelling, log_for_label_addition


def remove_label_from_superevent(labelling, user, add_log_message=True,
    issue_alert=True):

    # Delete Labelling object
    labelling.delete()

    log_for_label_removal = None
    if add_log_message:
        # Record label addition in superevent logs
        comment = 'Removed label: {label_name}'.format(
            label_name=labelling.label.name)
        log_for_label_removal = create_log(user, comment, labelling.superevent,
            issue_alert=False, autogenerated=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)

    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)
    if created and display_name is not None:
        tag.displayName = display_name
        tag.save()

    return tag


def get_or_create_tags(tag_name_list, display_name_list=[]):

    # TODO: make this a useful error
    if display_name_list and (len(display_name_list) != len(tag_name_list)):
        raise ValueError('')

    tag_list = []
    for i, tag_name in enumerate(tag_name_list):
        display_name = None
        if display_name_list:
            display_name = display_name_list[i]
        tag = get_or_create_tag(tag_name, display_name)
        tag_list.append(tag)

    return tag_list


# TODO: add permissions checking?
# TODO: move this somewhere else?
def get_superevent_by_date_id_or_404(superevent_id, queryset=None):

    try:
        filter_kwargs = Superevent.get_filter_kwargs_for_date_id_lookup(
            superevent_id)
    except Superevent.DateIdError as e:
        # The user passed an invalid date string (i.e., month=13
        # or something).  Probably should return 400 when this happens,
        # since it is technically a client error, but it's a lot simpler to
        # just raise a 404 here than to wrap every usage of this function in a
        # try-except block (or more than one, if it's in a CBV) and return a
        # 400. But raising a 404 is not technically wrong.
        raise Http404(e)

    # TODO: filter queryset for user here
    if queryset is None:
        queryset = Superevent.objects.all()

    return get_object_or_404(queryset, **filter_kwargs)


def confirm_superevent_as_gw(superevent, user, add_log_message=True,
    issue_alert=True):

    # Update superevent (mark as a GW, construct new ID, etc.)
    superevent.confirm_as_gw()

    # Create log message
    gw_log = None
    if add_log_message:
        message = "Confirmed as a gravitational wave."
        gw_log = create_log(user, message, superevent, issue_alert=issue_alert,
            autogenerated=False)

    # TODO: add label?

    return gw_log


def create_emobservation_for_superevent(superevent, submitter, ra_list,
    dec_list, ra_width_list, dec_width_list, start_time_list, duration_list,
    emgroup, comment="", add_log_message=True, issue_alert=True):

    # Create EMObservation
    emo = EMObservation.objects.create(submitter=submitter,
        superevent=superevent, group=emgroup, comment=comment)

    # Create EMFootprint objects
    for ra, dec, ra_width, dec_width, start_time, exposure_time in \
        zip(ra_list, dec_list, ra_width_list, dec_width_list, start_time_list, duration_list):

        emf = EMFootprint.objects.create(observation=emo, ra=ra, dec=dec,
            raWidth=ra_width, decWidth=dec_width, start_time=start_time,
            exposure_time=exposure_time)

    # Calculation covering region for observation
    emo.calculateCoveringRegion()
    emo.save()

    # Create a log message
    if add_log_message:
        message = "New EMBB observation record for {0}".format(emgroup.name)
        gw_log = create_log(submitter, message, superevent, issue_alert=False,
            autogenerated=False)

    # Issue alert
    if issue_alert:
        issue_alert_for_superevent_emobservation(emo)

    return emo


def create_voevent_for_superevent(superevent, issuer, voevent_type,
    skymap_type=None, skymap_filename=None, skymap_image_filename=None,
    internal=True, vetted=False, open_alert=False, hardware_inj=False,
    CoincComment=False, ProbHasNS=None, ProbHasRemnant=None,
    add_log_message=True, issue_alert=True):

    # Instantiate VOEvent object
    voevent = VOEvent.objects.create(superevent=superevent, issuer=issuer,
        voevent_type=voevent_type)

    # Construct VOEvent file text
    voevent_text, ivorn = construct_voevent_file(superevent, voevent,
        skymap_type=skymap_type, skymap_filename=skymap_filename,
        skymap_image_filename=skymap_image_filename, internal=internal,
        vetted=vetted, open_alert=open_alert, hardware_inj=hardware_inj,
        CoincComment=CoincComment, ProbHasNS=ProbHasNS,
        ProbHasRemnant=ProbHasRemnant)

    # Save versioned VOEvent file
    voevent_display_type = dict(VOEvent.VOEVENT_TYPE_CHOICES) \
        [voevent.voevent_type].capitalize()
    voevent_filename = "{superevent}-{N}-{voevent_type}.xml".format(
        superevent=superevent.superevent_id, N=voevent.N,
        voevent_type=voevent_display_type)
    version = create_versioned_file(voevent_filename, superevent.datadir,
        voevent_text)

    # Update VOEvent object
    voevent.filename = voevent_filename
    voevent.file_version = version
    voevent.ivorn = ivorn
    voevent.save(update_fields=['filename', 'file_version', 'ivorn'])

    # Create a log entry to document the new VOEvent (tag it as em_follow)
    if add_log_message:
        comment = "New VOEvent"
        em_follow = Tag.objects.get(name='em_follow')
        voevent_log = create_log(issuer, comment, superevent,
            filename=voevent.filename, file_version=voevent.file_version,
            tags=[em_follow], issue_alert=False)

    # Issue an alert
    if issue_alert:
        issue_alert_for_superevent_voevent(voevent)

    return voevent


# TODO: wrap this function in a try-except block in the form
def create_signoff_for_superevent(superevent, user, signoff_type,
    signoff_instrument, signoff_status, signoff_comment, add_log_message=True,
    issue_alert=True):

    # Create signoff
    signoff = Signoff.objects.create(superevent=superevent, submitter=user,
        instrument=signoff_instrument, signoff_type=signoff_type,
        status=signoff_status, comment=signoff_comment)

    # Create log message to document the signoff
    if add_log_message:
        signoff_type_full = dict(Signoff.SIGNOFF_TYPE_CHOICES)[signoff_type]
        comment = "{signoff_type} signoff certified status as {status}".format(
            signoff_type=signoff_type_full.capitalize(), status=signoff_status)
        if signoff_instrument:
            comment += " for {inst}".format(inst=signoff_instrument)
        em_follow = Tag.objects.get(name='em_follow')
        signoff_log = create_log(user, comment, superevent, issue_alert=False,
            tags=[em_follow])

    # Remove label which requested signoff
    labelling_to_remove = superevent.labelling_set.filter(label__name=
        signoff.get_req_label_name()).first()
    if labelling_to_remove is not None:
        remove_label_from_superevent(labelling_to_remove, user)

    # Add new label depending on signoff status
    label_to_add = Label.objects.get(name=signoff.get_status_label_name())
    add_label_to_superevent(superevent, label_to_add, user)

    # Issue alert
    if issue_alert:
        issue_alert_for_superevent_signoff(signoff)

    return signoff

def update_signoff_for_superevent(signoff, user, changed_data,
    add_log_message=True, issue_alert=True):
    # changed_data is a dict which contains the fields of the signoff
    # which have changed

    # Get superevent
    superevent = signoff.superevent

    # Save signoff
    signoff.save()

    if 'status' in changed_data:
        # If label for opposite status exists, remove it (i.e., the status has
        # changed in this update, so we need to update the labels)
        labelling_to_remove = superevent.labelling_set.filter(label__name=
            signoff.get_opposite_status_label_name()).first()
        if labelling_to_remove is not None:
            remove_label_from_superevent(labelling_to_remove, user,
                add_log_message=False, issue_alert=True)

        # If label for current status doesn't exist, add it
        label_to_add = Label.objects.get(name=signoff.get_status_label_name())
        if label_to_add not in superevent.labels.all():
            add_label_to_superevent(superevent, label_to_add, user,
                add_log_message=False, issue_alert=True)

    # Create log message to document the update
    if add_log_message:
        # Construct message
        signoff_type_full = dict(Signoff.SIGNOFF_TYPE_CHOICES) \
            [signoff.signoff_type]
        comment = "{signoff_type} signoff updated".format(
            signoff_type=signoff_type_full.capitalize())
        if signoff.instrument:
            comment += " for {inst}".format(inst=signoff.instrument)
        if 'status' in changed_data:
            comment += (": status {old_status} -> {new_status}, label "
                "{r_label} removed, and label {a_label} applied").format(
                old_status=signoff.opposite_status, new_status=signoff.status,
                r_label=labelling_to_remove.label.name,
                a_label=label_to_add.name)
        em_follow = Tag.objects.get(name='em_follow')
        signoff_log = create_log(user, comment, superevent, issue_alert=False,
            tags=[em_follow])

    # Issue alert
    if issue_alert:
        issue_alert_for_superevent_signoff(signoff)

    return signoff


def delete_signoff_for_superevent(signoff, user, add_log_message=True,
    issue_alert=True):

    # Get superevent
    superevent = signoff.superevent

    # Delete signoff
    signoff.delete()

    # Remove current OK or NO label: we don't add a log message here since
    # we'll document this in the full log message about the signoff
    labelling_to_remove = superevent.labelling_set.filter(label__name=
        signoff.get_status_label_name()).first()
    remove_label_from_superevent(labelling_to_remove, user,
        add_log_message=False, issue_alert=True)

    # Reapply initial "req" labe: we don't add a log message here since we'll
    # document this in the full log message about the signoff
    label_to_add = Label.objects.get(name=signoff.get_req_label_name())
    add_label_to_superevent(superevent, label_to_add, user,
        add_log_message=False, issue_alert=True)

    # Log message
    if add_log_message:
        # Construct log message
        signoff_type_full = dict(Signoff.SIGNOFF_TYPE_CHOICES) \
            [signoff.signoff_type]
        comment = "{signoff_type} signoff ".format(signoff_type=
            signoff_type_full.capitalize())
        if signoff.instrument:
            comment += "for {inst} ".format(inst=signoff.instrument)
        comment += "deleted: {r_label} removed and {a_label} reapplied".format(
            r_label=labelling_to_remove.label.name, a_label=label_to_add.name)
        em_follow = Tag.objects.get(name='em_follow')
        signoff_log = create_log(user, comment, superevent, tags=[em_follow],
            issue_alert=False)

    # Alert
    if issue_alert:
        issue_alert_for_superevent_log(signoff_log)