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>
<div id="tabs" style="min-width: 300px; max-width: 400px; display: none;">
<ul>
{% for form in forms %}
<li><a href="#tab-{{ forloop.counter }}">{{ form.key|title }} alert</a></li>
{% endfor %}
</ul>
{% for form in forms %}
<div id="tab-{{ forloop.counter }}">
<form method="POST" class="contactform">
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit"/>
</form>
</table>
<input type="submit" value="Submit" />