views.py 9.98 KB
Newer Older
1 2
"""Flask web application views."""
import datetime
3
import platform
4
import re
5
import socket
6
import sys
7

8 9 10 11 12 13
try:
    from importlib import metadata
except ImportError:
    # FIXME Remove when we drop support for Python < 3.7
    import importlib_metadata as metadata

14 15
from astropy.time import Time
from flask import flash, jsonify, redirect, render_template, request, url_for
16
from flask import make_response
17
from requests.exceptions import HTTPError
18

19
from . import app as celery_app
20
from ._version import get_versions
21
from .flask import app, cache
22
from .tasks import first2years, gracedb, orchestrator, circulars, superevents
23 24
from .util import PromiseProxy

25
distributions = PromiseProxy(lambda: tuple(metadata.distributions()))
26 27 28 29


@app.route('/')
def index():
30
    """Render main page."""
31
    return render_template(
32 33
        'index.jinja2',
        conf=celery_app.conf,
34
        hostname=socket.getfqdn(),
Leo Pound Singer's avatar
Leo Pound Singer committed
35
        distributions=distributions,
36
        platform=platform.platform(),
37 38
        versions=get_versions(),
        python_version=sys.version)
39 40 41


def take_n(n, iterable):
42
    """Take the first `n` items of a collection."""
43 44 45 46 47 48 49
    for i, item in enumerate(iterable):
        if i >= n:
            break
        yield item


# Regular expression for parsing query strings
50
# that look like GraceDB superevent names.
51
_typeahead_superevent_id_regex = re.compile(
52 53 54 55 56 57 58
    r'(?P<prefix>[MT]?)S?(?P<date>\d{0,6})(?P<suffix>[a-z]*)',
    re.IGNORECASE)


@app.route('/typeahead_superevent_id')
@cache.cached(query_string=True)
def typeahead_superevent_id():
59
    """Search GraceDB for superevents by ID.
60

61
    This involves some date parsing because GraceDB does not support directly
62 63
    searching for superevents by ID substring.
    """
64 65 66 67
    max_results = 8  # maximum number of results to return
    batch_results = 32  # batch size for results from server

    term = request.args.get('superevent_id')
68
    match = _typeahead_superevent_id_regex.fullmatch(term) if term else None
69 70

    if match:
71
        # Determine GraceDB event category from regular expression.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
        prefix = match['prefix'].upper() + 'S'
        category = {'T': 'test', 'M': 'MDC'}.get(
            match['prefix'].upper(), 'production')

        # Determine start date from regular expression by padding out
        # the partial date with missing digits defaulting to 000101.
        date_partial = match['date']
        date_partial_length = len(date_partial)
        try:
            date_start = datetime.datetime.strptime(
                date_partial + '000101'[date_partial_length:], '%y%m%d')
        except ValueError:  # invalid date
            return jsonify([])

        # Determine end date from regular expression by adding a very
        # loose upper bound on the number of days until the next
        # digit in the date rolls over. No need to be exact here.
        date_end = date_start + datetime.timedelta(
            days=[36600, 3660, 366, 320, 32, 11, 1.1][date_partial_length])

92
        # Determine GraceDB event suffix from regular expression.
93 94 95 96 97 98 99 100 101 102
        suffix = match['suffix'].lower()
    else:
        prefix = 'S'
        category = 'production'
        date_end = datetime.datetime.utcnow()
        date_start = date_end - datetime.timedelta(days=7)
        date_partial = ''
        date_partial_length = 0
        suffix = ''

103
    # Query GraceDB.
104 105
    query = 'category: {} t_0: {} .. {}'.format(
        category, Time(date_start).gps, Time(date_end).gps)
106 107
    response = gracedb.client.superevents.search(
        query=query, sort='superevent_id', count=batch_results)
108 109 110 111 112 113 114 115 116 117 118 119

    # Filter superevent IDs that match the search term.
    regex = re.compile(r'{}{}\d{{{}}}{}[a-z]*'.format(
        prefix, date_partial, 6 - date_partial_length, suffix))
    superevent_ids = (
        superevent['superevent_id'] for superevent
        in response if regex.fullmatch(superevent['superevent_id']))

    # Return only the first few matches.
    return jsonify(list(take_n(max_results, superevent_ids)))


120 121 122
@app.route('/typeahead_event_id')
@cache.cached(query_string=True)
def typeahead_event_id():
123
    """Search GraceDB for events by ID."""
124 125 126 127 128 129 130 131 132
    superevent_id = request.args.get('superevent_id').strip()
    query_terms = [f'superevent: {superevent_id}']
    if superevent_id.startswith('T'):
        query_terms.append('Test')
    elif superevent_id.startswith('M'):
        query_terms.append('MDC')
    query = ' '.join(query_terms)
    try:
        results = gracedb.get_events(query)
133
    except HTTPError:
134 135 136 137
        results = []
    results = [dict(r, snr=superevents.get_snr(r)) for r in results
               if superevents.is_complete(r)]
    return jsonify(list(reversed(sorted(results, key=superevents.keyfunc))))
138 139


140 141
def _search_by_tag_and_filename(superevent_id, filename, extension, tag):
    try:
142
        records = gracedb.get_log(superevent_id)
143
        return [
144 145
            '{},{}'.format(record['filename'], record['file_version'])
            for record in records if tag in record['tag_names']
146 147
            and record['filename'].startswith(filename)
            and record['filename'].endswith(extension)]
148
    except HTTPError as e:
149
        # Ignore 404 errors from server
150
        if e.response.status_code == 404:
151 152 153 154 155 156 157 158
            return []
        else:
            raise


@app.route('/typeahead_skymap_filename')
@cache.cached(query_string=True)
def typeahead_skymap_filename():
159
    """Search for sky maps by filename."""
160 161 162 163 164 165 166
    return jsonify(_search_by_tag_and_filename(
        request.args.get('superevent_id') or '',
        request.args.get('filename') or '',
        '.fits.gz', 'sky_loc'
    ))


167
@app.route('/typeahead_em_bright_filename')
168
@cache.cached(query_string=True)
169
def typeahead_em_bright_filename():
170
    """Search em_bright files by filename."""
171 172 173 174 175 176 177 178 179 180
    return jsonify(_search_by_tag_and_filename(
        request.args.get('superevent_id') or '',
        request.args.get('filename') or '',
        '.json', 'em_bright'
    ))


@app.route('/typeahead_p_astro_filename')
@cache.cached(query_string=True)
def typeahead_p_astro_filename():
181
    """Search p_astro files by filename."""
182 183 184 185 186 187 188
    return jsonify(_search_by_tag_and_filename(
        request.args.get('superevent_id') or '',
        request.args.get('filename') or '',
        '.json', 'p_astro'
    ))


189 190
@app.route('/send_preliminary_gcn', methods=['POST'])
def send_preliminary_gcn():
191
    """Handle submission of preliminary alert form."""
192 193 194 195
    keys = ('superevent_id', 'event_id')
    superevent_id, event_id, *_ = tuple(request.form.get(key) for key in keys)
    if superevent_id and event_id:
        (
196 197 198 199 200 201 202
            gracedb.upload.s(
                None, None, superevent_id,
                'User {} queued a Preliminary alert through the dashboard.'
                .format(request.remote_user or '(unknown)'),
                tags=['em_follow'])
            |
            gracedb.update_superevent.si(
203 204 205 206 207 208 209 210 211 212 213 214 215
                superevent_id, preferred_event=event_id)
            |
            gracedb.get_event.si(event_id)
            |
            orchestrator.preliminary_alert.s(superevent_id)
        ).delay()
        flash('Queued preliminary alert for {}.'.format(superevent_id),
              'success')
    else:
        flash('No alert sent. Please fill in all fields.', 'danger')
    return redirect(url_for('index'))


216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
@app.route('/change_prefered_event', methods=['POST'])
def change_prefered_event():
    """Handle submission of preliminary alert form."""
    keys = ('superevent_id', 'event_id')
    superevent_id, event_id, *_ = tuple(request.form.get(key) for key in keys)
    if superevent_id and event_id:
        (
            gracedb.upload.s(
                None, None, superevent_id,
                'User {} queued a prefered event change to {}.'
                .format(request.remote_user or '(unknown)', event_id),
                tags=['em_follow'])
            |
            gracedb.update_superevent.si(
                superevent_id, preferred_event=event_id)
            |
            gracedb.get_event.si(event_id)
            |
            orchestrator.preliminary_alert.s(
                superevent_id, initiate_voevent=False)
        ).delay()
        flash('Changed prefered event for {}.'.format(superevent_id),
              'success')
    else:
        flash('No change performed. Please fill in all fields.', 'danger')
    return redirect(url_for('index'))


244 245
@app.route('/send_update_gcn', methods=['POST'])
def send_update_gcn():
246
    """Handle submission of update alert form."""
247
    keys = ('superevent_id', 'skymap_filename',
248
            'em_bright_filename', 'p_astro_filename')
249 250
    superevent_id, *filenames = args = tuple(
        request.form.get(key) for key in keys)
251
    if all(args):
252 253 254
        (
            gracedb.upload.s(
                None, None, superevent_id,
Leo Pound Singer's avatar
Leo Pound Singer committed
255
                'User {} queued an Update alert through the dashboard.'
256 257 258
                .format(request.remote_user or '(unknown)'),
                tags=['em_follow'])
            |
259
            orchestrator.update_alert.si(filenames, superevent_id)
260
        ).delay()
261 262 263 264 265 266
        flash('Queued update alert for {}.'.format(superevent_id), 'success')
    else:
        flash('No alert sent. Please fill in all fields.', 'danger')
    return redirect(url_for('index'))


267 268
@app.route('/create_update_gcn_circular', methods=['POST'])
def create_update_gcn_circular():
269
    """Handle submission of GCN Circular form."""
270 271 272 273 274
    keys = ['sky_localization', 'em_bright', 'p_astro']
    superevent_id = request.form.get('superevent_id')
    updates = [key for key in keys if request.form.get(key)]
    if superevent_id and updates:
        response = make_response(circulars.create_update_circular(
275 276
            superevent_id,
            update_types=updates))
277 278 279 280 281 282 283 284
        response.headers["content-type"] = "text/plain"
        return response
    else:
        flash('No circular created. Please fill in superevent ID and at ' +
              'least one update type.', 'danger')
    return redirect(url_for('index'))


285 286
@app.route('/send_mock_event', methods=['POST'])
def send_mock_event():
287
    """Handle submission of mock alert form."""
288 289 290
    first2years.upload_event.delay()
    flash('Queued a mock event.', 'success')
    return redirect(url_for('index'))