Skip to content
Snippets Groups Projects
Commit 8cee1ae4 authored by Tanner Prestegard's avatar Tanner Prestegard Committed by GraceDB
Browse files

Updates to notification form views, cleaning, etc.

* Filter available contacts for a notification to be verified
  contacts belonging to the user
* New better code for handling label queries
* Some better help text for notification forms
* Remove old notification forms and views
parent c0dbe5ab
No related branches found
No related tags found
No related merge requests found
from __future__ import absolute_import
from collections import defaultdict
import logging
from pyparsing import ParseException
import pyparsing
import textwrap
from django import forms
from django.core.exceptions import NON_FIELD_ERRORS
from django.db.models import Q
from django.forms.utils import ErrorList
from django.utils import timezone
from django.utils.encoding import force_text
......@@ -12,152 +15,113 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from core.forms import MultipleForm
from search.query.labels import parseLabelQuery
from events.models import Group, Search, Label
from .models import Notification, Contact
from .utils import parse_label_query
# Set up logger
logger = logging.getLogger(__name__)
class CleanNotificationFormMixin(object):
###############################################################################
# Notification forms ##########################################################
###############################################################################
class BaseNotificationForm(forms.ModelForm):
"""
Base model for Notification forms. Should not be used on its own
(essentially an abstract model)
"""
class Meta:
model = Notification
fields = ['description'] # dummy placeholder
labels = {
'far_threshold': 'FAR Threshold (Hz)',
}
help_texts = {
'contacts': ('If this box is empty, you must create and verify a '
'contact.'),
'label_query': textwrap.dedent("""\
Label names can be combined with binary AND: ('&' or ',')
or binary OR: '|'. They can also be negated with '~' or '-'.
For N labels, there must be exactly N-1 binary operators.
Parentheses are not allowed.
""").rstrip()
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super(BaseNotificationForm, self).__init__(*args, **kwargs)
# Dynamically set contacts queryset to be only contacts that:
# a) belong to the user
# b) are verified
if user is not None:
self.fields['contacts'].queryset = user.contact_set.filter(
verified=True)
def clean(self):
data = super(CleanNotificationFormMixin, self).clean()
return data
cleaned_data = super(BaseNotificationForm, self).clean()
# Dict for holding errors. Keys are class members,
# values are lists of error messages
err_dict = defaultdict(list)
# Try to get fields from cleaned data
label_query = cleaned_data.get('label_query', None)
labels = cleaned_data.get('labels', None)
# Can't specify a label from the list and a label query
if label_query is not None and labels is not None:
err_msg = ('Cannot specify both labels and label query, '
'choose one or the other.')
err_dict[NON_FIELD_ERRORS].append(err_msg)
def clean_label_query(self):
label_query = self.cleaned_data['label_query']
return label_query
# If there is a label query, get the labels involved and store them
# in the labels attribute. We use this in the alert generation code
# as an easy way of checking whether a notification might be triggered.
if label_query is not None:
try:
labels = parse_label_query(label_query)
except pyparsing.ParseException:
err_dict['label_query'].append('Invalid label query.')
else:
cleaned_data['labels'] = Label.objects.filter(name__in=labels)
# Raise errors, if any
if err_dict:
raise forms.ValidationError(err_dict)
class SupereventNotificationForm(forms.ModelForm, MultipleForm,
CleanNotificationFormMixin):
return cleaned_data
class SupereventNotificationForm(BaseNotificationForm, MultipleForm):
key = 'superevent'
category = Notification.NOTIFICATION_CATEGORY_SUPEREVENT
class Meta:
model = Notification
class Meta(BaseNotificationForm.Meta):
fields = ['description', 'contacts', 'far_threshold', 'labels',
'label_query', 'ns_candidate', 'key_field']
class EventNotificationForm(forms.ModelForm, MultipleForm,
CleanNotificationFormMixin):
class EventNotificationForm(BaseNotificationForm, MultipleForm):
key = 'event'
category = Notification.NOTIFICATION_CATEGORY_EVENT
class Meta:
model = Notification
# Remove 'Test' group
groups = forms.ModelMultipleChoiceField(queryset=
Group.objects.exclude(name='Test'))
# Remove 'MDC' and 'O2VirgoTest' searches
searches = forms.ModelMultipleChoiceField(queryset=
Search.objects.exclude(name__in=['MDC', 'O2VirgoTest']))
class Meta(BaseNotificationForm.Meta):
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)',
required=False)
class Meta:
model = Notification
fields = ['contacts', 'pipelines', 'far_threshold', 'labels', 'label_query']
widgets = {'label_query': forms.TextInput(attrs={'size': 50})}
help_texts = {
'label_query': ("Label names can be combined with binary AND: "
"'&' or ','; or binary OR: '|'. For N "
"labels, there must be exactly N-1 binary "
"operators. Parentheses are not allowed. "
"Additionally, any of the labels in a query "
"string can be negated with '~' or '-'. "
"Labels can either be selected with the select"
" box at the top, or a query can be specified,"
" <i>but not both</i>."),
}
contacts = forms.ModelMultipleChoiceField(
queryset=Contact.objects.filter(user=user),
required=True,
help_text="If this box is empty, go back and create a contact first.",
error_messages={'required': 'You must specify at least one contact.'})
# XXX should probably override is_valid and check for
# truth of (atypes or labels)
# and set field error attributes appropriately.
def clean(self, *args, **kwargs):
cleaned_data = super(TF, self).clean(*args, **kwargs)
# Dict for holding errors. Keys are class members,
# values are lists of error messages
err_dict = defaultdict(list)
# Can't specify a label from the list and a label query
if (cleaned_data['label_query'] and cleaned_data['labels']):
err_msg = ('Cannot specify both labels and label query, '
'choose one or the other.')
err_dict[NON_FIELD_ERRORS].append(err_msg)
# Notifications currently require a label or a pipeline to be
# specified. In the future, we should also allow the cases which
# have only a FAR threshold or a label query
if not (cleaned_data['labels'] or cleaned_data['pipelines']):
err_msg = ('Choose labels and/or pipelines for this '
'notification.')
err_dict[NON_FIELD_ERRORS].append(err_msg)
# Make sure the label query is valid
if cleaned_data['label_query']:
# now try parsing it
try:
parseLabelQuery(cleaned_data['label_query'])
except ParseException:
err_dict['label_query'].append('Invalid label query')
# Raise errors, if any
if err_dict:
raise forms.ValidationError(err_dict)
return cleaned_data
def as_table(self, *args, **kwargs):
"""
Overriding default as_table method to put non-field errors
at the top of the table. Allows removal of "flash message box".
"""
# Get non-field errors and remove them from the error list
# to prevent duplicates.
nfe = self.non_field_errors()
self.errors[NON_FIELD_ERRORS] = []
# Generate table HTML and add non-field errors to beginning row
table_data = super(TF, self).as_table(*args, **kwargs)
if nfe:
table_data = '\n<tr><td colspan="2">\n' + \
process_errors(nfe) + '\n</td></tr>\n' + table_data
return mark_safe(table_data)
if postdata is not None:
return TF(postdata)
else:
return TF()
def process_errors(err):
"""Processes and formats errors in ContactForms."""
out_errs = []
if isinstance(err,ErrorList):
for e in err:
out_errs.append('<p class="error">{0}</p>' \
.format(conditional_escape(e)))
elif isinstance(err,str):
out_errs.append('<p class="error">{0}</p>' \
.format(conditional_escape(err)))
else:
out_errs.append(force_text(err))
return "\n".join(out_errs)
###############################################################################
# Contact forms ###############################################################
###############################################################################
class PhoneContactForm(forms.ModelForm, MultipleForm):
key = 'phone'
......
from __future__ import absolute_import
from pyparsing import oneOf, Literal, Optional, ZeroOrMore, StringEnd, Suppress
from django.db.models import Q
from events.models import Label
OPERATORS = {
'AND': oneOf(", &"),
'OR': Literal("|"),
'NOT': oneOf("- ~"),
}
def parse_label_query(s):
"""Parses a label query into a list of label names"""
# Parser for one label name
label = oneOf(list(Label.objects.all().values_list('name', flat=True)))
# "intermediate" parser - between labels should be AND or OR and then
# an optional NOT
im = Suppress((OPERATORS['AND'] ^ OPERATORS['OR']) +
Optional(OPERATORS['NOT']))
# Full parser: optional NOT and a label, then zero or more
# "intermediate" + label combos, then string end
labelQ = Suppress(Optional(OPERATORS['NOT'])) + label + \
ZeroOrMore(im + label) + StringEnd()
return labelQ.parseString(s).asList()
......@@ -5,7 +5,6 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.mail import EmailMessage
from django.db.models import Q
from django.http import (
HttpResponse, HttpResponseRedirect, HttpResponseNotFound,
Http404, HttpResponseForbidden, HttpResponseBadRequest
......@@ -27,19 +26,16 @@ from core.views import MultipleFormView
from events.permission_utils import lvem_user_required, is_external
from events.models import Label
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
from .phone import get_twilio_from
# Set up logger
logger = log = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
@login_required
......@@ -106,6 +102,12 @@ class CreateNotificationView(MultipleFormView):
kwargs['idx'] = idx
return kwargs
def get_form_kwargs(self, *args, **kwargs):
kw = super(CreateNotificationView, self).get_form_kwargs(
*args, **kwargs)
kw['user'] = self.request.user
return kw
def form_valid(self, form):
if form.cleaned_data.has_key('key_field'):
form.cleaned_data.pop('key_field')
......@@ -139,6 +141,12 @@ class EditNotificationView(UpdateView):
else:
return SupereventNotificationForm
def get_form_kwargs(self, *args, **kwargs):
kw = super(EditNotificationView, self).get_form_kwargs(
*args, **kwargs)
kw['user'] = self.request.user
return kw
def get_queryset(self):
return self.request.user.notification_set.all()
......@@ -166,66 +174,6 @@ class DeleteNotificationView(DeleteView):
return self.request.user.notification_set.all()
@internal_user_required
def create(request):
"""Create a notification (Notification) via the web interface"""
if request.method == "POST":
form = notificationFormFactory(request.POST, user=request.user)
if form.is_valid():
# Create the Notification
t = Notification(user=request.user)
labels = form.cleaned_data['labels']
pipelines = form.cleaned_data['pipelines']
contacts = form.cleaned_data['contacts']
far_threshold = form.cleaned_data['far_threshold']
label_query = form.cleaned_data['label_query']
# TODO: properly handle negated labels
# If we've got a label query defined for this notification, then we want
# each label mentioned in the query to be listed in the event's
# labels. It would be smarter to make sure the label isn't being
# negated, but we can just leave that for later.
if len(label_query) > 0:
toks = labelQuery(label_query, names=True)
f = Q()
for tok in toks:
# Note that all labels are being combined with OR
if isinstance(tok,Q):
f = f | tok
if len(f)==0:
return HttpResponseBadRequest("Please enter a valid label query.")
labels = Label.objects.filter(f)
# If the form is valid, then we have at least one contact and
# either a label or pipeline.
# So let's create the contact object.
# Can't access many-to-many relationships before object is saved
t.save()
# Now populate fields
try:
t.labels = labels
t.pipelines = pipelines
t.contacts = contacts
t.far_threshold = far_threshold
t.label_query = label_query
t.save()
messages.info(request, 'Created notification: {n}.'.format(
n=t.display()))
except Exception as e:
messages.error(request, ('Error creating notification {n}: '
'{e}.').format(n=t.display(), e=e))
t.delete()
return HttpResponseRedirect(reverse('alerts:index'))
else:
form = notificationFormFactory(user=request.user)
return render(request, 'profile/createNotification.html',
context={"form": form})
###############################################################################
# Contact views ###############################################################
###############################################################################
......
......@@ -22,7 +22,8 @@ class MultipleFormView(FormView):
def get_forms(self, form_classes=None):
if (form_classes is None):
form_classes = self.form_classes
return [form(**self.get_form_kwargs(form.key)) for form in form_classes]
return [form(**self.get_form_kwargs(form.key)) for form in
form_classes]
def get_form_kwargs(self, form_key):
kwargs = {
......
......@@ -42,7 +42,7 @@ $(document).ready(function() {
<li>Phone and email notifications will not be issued outside of observing runs, in order to avoid potential accidents.</li>
</ul>
</div>
<div id="tabs" style="min-width: 300px; max-width: 400px; display: none;">
<div id="tabs" style="min-width: 300px; max-width: 500px; display: none;">
<ul>
{% for form in forms %}
<li><a href="#tab-{{ forloop.counter }}">{{ form.key|title }} alert</a></li>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment