Maintenance will be performed on git.ligo.org, chat.ligo.org, containers.ligo.org, and docs.ligo.org on Tuesday 26 May 2020 starting at approximately 10am CDT. It is expected to take around 30 minutes and will involve a short period of downtime, around 5 minutes, towards the end of the maintenance period. Please address any questions, comments, or concerns to uwm-help@cgca.uwm.edu.

Commit 7108b081 authored by Tanner Prestegard's avatar Tanner Prestegard Committed by GraceDB

Overhaul of notifications

New model functionality, better forms and views.
parent ac36ea55
......@@ -11,4 +11,3 @@ class NotificationManager(admin.ModelAdmin):
admin.site.register(Contact, ContactManager)
admin.site.register(Notification, NotificationManager)
......@@ -19,6 +19,40 @@ from .models import Notification, Contact
logger = logging.getLogger(__name__)
class CleanNotificationFormMixin(object):
def clean(self):
data = super(CleanNotificationFormMixin, self).clean()
return data
def clean_label_query(self):
label_query = self.cleaned_data['label_query']
return label_query
class SupereventNotificationForm(forms.ModelForm, MultipleForm,
CleanNotificationFormMixin):
key = 'superevent'
category = Notification.NOTIFICATION_CATEGORY_SUPEREVENT
class Meta:
model = Notification
fields = ['description', 'contacts', 'far_threshold', 'labels',
'label_query', 'ns_candidate', 'key_field']
class EventNotificationForm(forms.ModelForm, MultipleForm,
CleanNotificationFormMixin):
key = 'event'
category = Notification.NOTIFICATION_CATEGORY_EVENT
class Meta:
model = Notification
fields = ['description', 'contacts', 'far_threshold', 'groups',
'pipelines', 'searches', 'labels', 'label_query', 'ns_candidate',
'key_field']
def notificationFormFactory(postdata=None, user=None):
class TF(forms.ModelForm):
far_threshold = forms.FloatField(label='FAR Threshold (Hz)',
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-31 19:16
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('events', '0031_hwinj_labels'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('alerts', '0008_auto_20190123_2120'),
]
operations = [
migrations.CreateModel(
name='EventNotification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('far_threshold', models.FloatField(blank=True, null=True)),
('label_query', models.CharField(blank=True, max_length=100, null=True)),
('ns_candidate', models.BooleanField(default=False)),
('contacts', models.ManyToManyField(to='alerts.Contact')),
('groups', models.ManyToManyField(to='events.Group')),
('labels', models.ManyToManyField(to='events.Label')),
('pipelines', models.ManyToManyField(to='events.Pipeline')),
('searches', models.ManyToManyField(to='events.Search')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SupereventNotification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('far_threshold', models.FloatField(blank=True, null=True)),
('label_query', models.CharField(blank=True, max_length=100, null=True)),
('contacts', models.ManyToManyField(to='alerts.Contact')),
('labels', models.ManyToManyField(to='events.Label')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.RemoveField(
model_name='notification',
name='contacts',
),
migrations.RemoveField(
model_name='notification',
name='labels',
),
migrations.RemoveField(
model_name='notification',
name='pipelines',
),
migrations.RemoveField(
model_name='notification',
name='user',
),
migrations.DeleteModel(
name='Notification',
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-31 19:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0009_auto_20190131_1916'),
]
operations = [
migrations.AddField(
model_name='supereventnotification',
name='ns_candidate',
field=models.BooleanField(default=False),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-31 19:53
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0010_supereventnotification_ns_candidate'),
]
operations = [
migrations.AlterField(
model_name='eventnotification',
name='groups',
field=models.ManyToManyField(blank=True, to='events.Group'),
),
migrations.AlterField(
model_name='eventnotification',
name='labels',
field=models.ManyToManyField(blank=True, to='events.Label'),
),
migrations.AlterField(
model_name='eventnotification',
name='pipelines',
field=models.ManyToManyField(blank=True, to='events.Pipeline'),
),
migrations.AlterField(
model_name='eventnotification',
name='searches',
field=models.ManyToManyField(blank=True, to='events.Search'),
),
migrations.AlterField(
model_name='supereventnotification',
name='labels',
field=models.ManyToManyField(blank=True, to='events.Label'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-31 20:27
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('events', '0031_hwinj_labels'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('alerts', '0011_auto_20190131_1953'),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('far_threshold', models.FloatField(blank=True, null=True)),
('label_query', models.CharField(blank=True, max_length=100, null=True)),
('category', models.CharField(choices=[(b'E', b'Event'), (b'S', b'Superevent')], default=b'S', max_length=1)),
('ns_candidate', models.BooleanField(default=False)),
('contacts', models.ManyToManyField(to='alerts.Contact')),
('groups', models.ManyToManyField(blank=True, to='events.Group')),
('labels', models.ManyToManyField(blank=True, to='events.Label')),
('pipelines', models.ManyToManyField(blank=True, to='events.Pipeline')),
('searches', models.ManyToManyField(blank=True, to='events.Search')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.RemoveField(
model_name='eventnotification',
name='contacts',
),
migrations.RemoveField(
model_name='eventnotification',
name='groups',
),
migrations.RemoveField(
model_name='eventnotification',
name='labels',
),
migrations.RemoveField(
model_name='eventnotification',
name='pipelines',
),
migrations.RemoveField(
model_name='eventnotification',
name='searches',
),
migrations.RemoveField(
model_name='eventnotification',
name='user',
),
migrations.RemoveField(
model_name='supereventnotification',
name='contacts',
),
migrations.RemoveField(
model_name='supereventnotification',
name='labels',
),
migrations.RemoveField(
model_name='supereventnotification',
name='user',
),
migrations.DeleteModel(
name='EventNotification',
),
migrations.DeleteModel(
name='SupereventNotification',
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-02-01 11:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0012_auto_20190131_2027'),
]
operations = [
migrations.AddField(
model_name='notification',
name='description',
field=models.CharField(default='description', max_length=30),
preserve_default=False,
),
migrations.AlterField(
model_name='contact',
name='description',
field=models.CharField(max_length=30),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-02-01 11:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0013_auto_20190201_1150'),
]
operations = [
migrations.AlterField(
model_name='notification',
name='description',
field=models.CharField(max_length=40),
),
]
......@@ -14,7 +14,6 @@ from django.utils.http import urlencode
from django_twilio.client import twilio_client
from core.models import CleanSaveModel
from events.models import Label, Pipeline
from .fields import PhoneNumberField
from .phone import get_twilio_from
......@@ -25,7 +24,9 @@ logger = logging.getLogger(__name__)
# Set up user model
UserModel = get_user_model()
###############################################################################
# Contacts ####################################################################
###############################################################################
class Contact(CleanSaveModel):
# Phone contact methods
CONTACT_PHONE_CALL = 'C'
......@@ -41,7 +42,7 @@ class Contact(CleanSaveModel):
# Fields
user = models.ForeignKey(UserModel, null=False)
description = models.CharField(max_length=20, blank=False, null=False)
description = models.CharField(max_length=30, blank=False, null=False)
email = models.EmailField(blank=True, null=True)
phone = PhoneNumberField(blank=True, max_length=255, null=True)
phone_method = models.CharField(max_length=1, null=True, blank=True,
......@@ -131,6 +132,17 @@ class Contact(CleanSaveModel):
self.verified = True
self.save(update_fields=['verified'])
def display(self):
if self.email:
return "Email {0}".format(self.email)
elif self.phone:
if self.phone_method == self.CONTACT_PHONE_BOTH:
return "Call and text {0}".format(self.phone)
elif self.phone_method == self.CONTACT_PHONE_CALL:
return "Call {0}".format(self.phone)
if self.phone_method == self.CONTACT_PHONE_TEXT:
return "Text {0}".format(self.phone)
def print_info(self):
"""Prints information about Contact object; useful for debugging."""
info_str = textwrap.dedent("""\
......@@ -144,33 +156,92 @@ class Contact(CleanSaveModel):
print(info_str)
###############################################################################
# Notifications ###############################################################
###############################################################################
class Notification(models.Model):
# Notification categories
NOTIFICATION_CATEGORY_EVENT = 'E'
NOTIFICATION_CATEGORY_SUPEREVENT = 'S'
NOTIFICATION_CATEGORY_CHOICES = (
(NOTIFICATION_CATEGORY_EVENT, 'Event'),
(NOTIFICATION_CATEGORY_SUPEREVENT, 'Superevent'),
)
user = models.ForeignKey(UserModel, null=False)
labels = models.ManyToManyField(Label, blank=True)
pipelines = models.ManyToManyField(Pipeline, blank=True)
contacts = models.ManyToManyField(Contact, blank=False)
contacts = models.ManyToManyField(Contact)
description = models.CharField(max_length=40, blank=False, null=False)
far_threshold = models.FloatField(blank=True, null=True)
label_query = models.CharField(max_length=100, blank=True)
labels = models.ManyToManyField('events.label', blank=True)
label_query = models.CharField(max_length=100, null=True, blank=True)
category = models.CharField(max_length=1, null=False, blank=False,
choices=NOTIFICATION_CATEGORY_CHOICES,
default=NOTIFICATION_CATEGORY_SUPEREVENT)
# Whether the event possibly has a neutron star in it.
# The logic for determining this is defined in a method below.
ns_candidate = models.BooleanField(default=False)
# Event-only fields
groups = models.ManyToManyField('events.group', blank=True)
pipelines = models.ManyToManyField('events.pipeline', blank=True)
searches = models.ManyToManyField('events.search', blank=True)
def __unicode__(self):
return (u"%s: %s") % (
self.user.username,
self.userlessDisplay()
self.display()
)
def userlessDisplay(self):
thresh = ""
def display(self):
kwargs = {}
if self.category == self.NOTIFICATION_CATEGORY_EVENT:
output = 'Event'
elif self.category == self.NOTIFICATION_CATEGORY_SUPEREVENT:
output = 'Superevent'
# Add label stuff
if self.label_query or self.labels.exists():
action = 'labeled with {labels}'
if self.label_query:
labels = '({0})'.format(self.label_query)
else:
labels = " | ".join([l.name for l in self.labels.all()])
if self.labels.count() > 1:
labels = '({0})'.format(labels)
action = action.format(labels=labels)
else:
action = 'created or updated'
output += ' {action}'.format(action=action)
# Add groups, pipelines, searches for event-type notifications
if self.category == self.NOTIFICATION_CATEGORY_EVENT:
output += ' & {groups} & {pipelines} & {searches}'
if self.groups.exists():
kwargs['groups'] = 'group=({0})'.format(
" | ".join([g.name for g in self.groups.all()]))
else:
kwargs['groups'] = 'any group'
if self.pipelines.exists():
kwargs['pipelines'] = 'pipeline=({0})'.format(
" | ".join([p.name for p in self.pipelines.all()]))
else:
kwargs['pipelines'] = 'any pipeline'
if self.searches.exists():
kwargs['searches'] = 'search=({0})'.format(
" | ".join([s.name for s in self.searches.all()]))
else:
kwargs['searches'] = 'any search'
# Optionally add FAR threshold
if self.far_threshold:
thresh = " & (far < %s)" % self.far_threshold
output += ' & (FAR < {far_threshold})'
kwargs['far_threshold'] = self.far_threshold
if self.label_query:
label_disp = self.label_query
else:
label_disp = "|".join([a.name for a in self.labels.all()]) or "creating"
# Optionally add NS candidate info
if self.ns_candidate:
output += ' & NS candidate'
return ("(%s) & (%s)%s -> %s") % (
"|".join([a.name for a in self.pipelines.all()]) or "any pipeline",
label_disp,
thresh,
", ".join([x.description for x in self.contacts.all()])
)
# Add contacts
output += ' -> {contacts}'
kwargs['contacts'] = \
", ".join([c.description for c in self.contacts.all()])
return output.format(**kwargs)
......@@ -25,11 +25,12 @@ urlpatterns = [
name="verify-contact"),
# Notifications
url(r'^notification/create/$', views.create, name="create-notification"),
url(r'^notification/delete/(?P<id>[\d]+)$', views.delete,
name="delete-notification"),
#url(r'^notification/edit/(?P<id>[\d]+)$', views.edit,
# name="edit-notification"),
url(r'^notification/create/$', views.CreateNotificationView.as_view(),
name="create-notification"),
url(r'^notification/(?P<pk>\d+)/edit/$',
views.EditNotificationView.as_view(), name="edit-notification"),
url(r'^notification/(?P<pk>\d+)/delete/$',
views.DeleteNotificationView.as_view(), name="delete-notification"),
# Manage password
url(r'^manage_password/$', views.managePassword,
......
......@@ -30,6 +30,7 @@ from ligoauth.decorators import internal_user_required
from search.query.labels import labelQuery
from .forms import (
PhoneContactForm, EmailContactForm, VerifyContactForm,
EventNotificationForm, SupereventNotificationForm,
notificationFormFactory,
)
from .models import Notification, Contact
......@@ -43,11 +44,12 @@ logger = log = logging.getLogger(__name__)
@login_required
def index(request):
notifications = Notification.objects.filter(user=request.user)
contacts = Contact.objects.filter(user=request.user)
d = { 'notifications': notifications, 'contacts': contacts }
context = {
'notifications': request.user.notification_set.all(),
'contacts': request.user.contact_set.all(),
}
return render(request, 'profile/notifications.html', context=d)
return render(request, 'profile/notifications.html', context=context)
@lvem_user_required
......@@ -86,6 +88,83 @@ def managePassword(request):
return render(request, 'profile/manage_password.html', context=d)
###############################################################################
# Notification views ##########################################################
###############################################################################
@method_decorator(internal_user_required, name='dispatch')
class CreateNotificationView(MultipleFormView):
"""Create a notification"""
template_name = 'alerts/create_notification.html'
success_url = reverse_lazy('alerts:index')
form_classes = [SupereventNotificationForm, EventNotificationForm]
def get_context_data(self, **kwargs):
kwargs['idx'] = 0
if (self.request.method in ('POST', 'PUT')):
form_keys = [f.key for f in self.form_classes]
idx = form_keys.index(self.request.POST['key_field'])
kwargs['idx'] = idx
return kwargs
def form_valid(self, form):
if form.cleaned_data.has_key('key_field'):
form.cleaned_data.pop('key_field')
# Add user (from request) and category (stored on form class) to
# the form instance, then save
form.instance.user = self.request.user
form.instance.category = form.category
form.save()
# Add message and return
messages.info(self.request, 'Created notification: {n}.'.format(
n=form.instance.description))
return super(CreateNotificationView, self).form_valid(form)
superevent_form_valid = event_form_valid = form_valid
@method_decorator(internal_user_required, name='dispatch')
class EditNotificationView(UpdateView):
"""Edit a notification"""
template_name = 'alerts/edit_notification.html'
# Have to provide form_class, but it will be dynamically selected below in
# get_form()
form_class = SupereventNotificationForm
success_url = reverse_lazy('alerts:index')
def get_form_class(self):
if self.object.category == Notification.NOTIFICATION_CATEGORY_EVENT:
return EventNotificationForm
else:
return SupereventNotificationForm
def get_queryset(self):
return self.request.user.notification_set.all()
@method_decorator(internal_user_required, name='dispatch')
class DeleteNotificationView(DeleteView):
"""Delete a notification"""
model = Notification
success_url = reverse_lazy('alerts:index')
def get(self, request, *args, **kwargs):
# Override this so that we don't require a confirmation page
# for deletion
return self.delete(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
response = super(DeleteNotificationView, self).delete(request, *args,
**kwargs)
messages.info(request, 'Notification {n} has been deleted.'.format(
n=self.object.description))
return response
def get_queryset(self):
# Queryset should only contain the user's notifications
return self.request.user.notification_set.all()
@internal_user_required
def create(request):
......@@ -134,10 +213,10 @@ def create(request):
t.label_query = label_query
t.save()
messages.info(request, 'Created notification: {n}.'.format(
n=t.userlessDisplay()))
n=t.display()))
except Exception as e:
messages.error(request, ('Error creating notification {n}: '
'{e}.').format(n=t.userlessDisplay(), e=e))
'{e}.').format(n=t.display(), e=e))
t.delete()
return HttpResponseRedirect(reverse('alerts:index'))
......@@ -146,24 +225,6 @@ def create(request):
return render(request, 'profile/createNotification.html',
context={"form": form})
@internal_user_required
def edit(request, id):
raise Http404
@internal_user_required
def delete(request, id):
try:
t = Notification.objects.get(id=id)
except Notification.DoesNotExist:
raise Http404
if request.user != t.user:
return HttpResponseForbidden(("You are not allowed to modify another "
"user's notifications."))
messages.info(request,'Notification "{nname}" has been deleted.' \
.format(nname=t.userlessDisplay()))
t.delete()
return HttpResponseRedirect(reverse('alerts:index'))
###############################################################################
# Contact views ###############################################################
......
......@@ -209,6 +209,22 @@ class Event(models.Model):
nodes.append(hdf.read())
return os.path.join(settings.GRACEDB_DATA_DIR, *nodes)
def is_ns_candidate(self):
# Used for notifications
# Current condition: m2 < 3.0 M_sun
# Ensure that we have the base event class
event = self
if hasattr(self, 'event_ptr'):
event = self.event_ptr
# Check for single inspirals
if event.singleinspiral_set.exists():
si = event.singleinspiral_set.first()
if (si.mass2 > 0 and si.mass2 < 3):
return True
return False
def is_test(self):
return self.group.name == 'Test'
......
{% extends "base.html" %}
{% load static %}
{% block headcontents %}
{{ block.super }}
<link rel="stylesheet" href="{% static "jquery-ui/themes/base/jquery-ui.min.css" %}" />
{% endblock %}
{% block jscript %}
<script src="{% static "jquery/dist/jquery.min.js" %}"></script>
<script src="{% static "jquery-ui/jquery-ui.min.js" %}"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#tabs").toggle();
$("#tabs").tabs({active: {{ idx }}});
});
</script>
{% endblock %}
{% block title %}Options | Create Notification{% endblock %}
{% block heading %}Create Notification{% endblock %}
......@@ -6,11 +23,6 @@
{% block content %}
<div style="color: red; padding-top: 20px;, padding-bottom: 5px;">
<h3>Status of notifications</h3>
<p>Notifications are <b>only</b> issued for events at present. Notifications for superevents will be added at the beginning of ER14.</p>
</div>
<br />
<div style="padding: 10px;">
<h3>Instructions</h3>
......@@ -30,12 +42,22 @@
<li>Phone and email notifications will not be issued outside of observing runs, in order to avoid potential accidents.</li>
</ul>
</div>
<form method="POST">
<table>