From 8a8fe4b8106a36720165350b13b6f46acd711b60 Mon Sep 17 00:00:00 2001
From: Tanner Prestegard <tanner.prestegard@ligo.org>
Date: Wed, 6 Mar 2019 09:33:12 -0600
Subject: [PATCH] Enhancements to Contact and Notification forms

---
 gracedb/alerts/forms.py | 89 ++++++++++++++++++++++++++++++++++++++---
 1 file changed, 83 insertions(+), 6 deletions(-)

diff --git a/gracedb/alerts/forms.py b/gracedb/alerts/forms.py
index 6933e4831..7b501d2a5 100644
--- a/gracedb/alerts/forms.py
+++ b/gracedb/alerts/forms.py
@@ -36,22 +36,46 @@ class BaseNotificationForm(forms.ModelForm):
         fields = ['description'] # dummy placeholder
         labels = {
             'far_threshold': 'FAR Threshold (Hz)',
+            'ns_candidate': 'Neutron star candidate',
         }
         help_texts = {
-            'contacts': ('If this box is empty, you must create and verify a '
-                'contact.'),
+            'contacts': textwrap.dedent("""\
+                Select a contact or contacts to receive the notification.
+                If this box is empty, you must create and verify a contact.
+            """).rstrip(),
+            'far_threshold': textwrap.dedent("""\
+                Require that the candidate has FAR less than this threshold.
+                Leave blank to place no requirement on FAR.
+            """).rstrip(),
+            'labels': textwrap.dedent("""\
+                Require that a label or labels must be attached to the
+                candidate. You can specify labels here or in the label query,
+                but not both.
+            """).rstrip(),
             'label_query': textwrap.dedent("""\
+                Require that the candidate's set of labels matches this query.
                 Label names can be combined with binary AND: ('&amp;' 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()
+            """).rstrip(),
+            'ns_candidate': ('Require that the candidate has m<sub>2</sub> '
+                '< 3.0 M<sub>sun</sub>.'),
+        }
+        widgets = {
+            'contacts': forms.widgets.SelectMultiple(attrs={'size': 5}),
+            'far_threshold': forms.widgets.TextInput(),
+            'labels': forms.widgets.SelectMultiple(attrs={'size': 8}),
         }
 
     def __init__(self, *args, **kwargs):
         user = kwargs.pop('user', None)
         super(BaseNotificationForm, self).__init__(*args, **kwargs)
 
+        # We need a user for this to work
+        assert user is not None
+        assert user.is_authenticated
+
         # Dynamically set contacts queryset to be only contacts that:
         #  a) belong to the user
         #  b) are verified
@@ -59,6 +83,10 @@ class BaseNotificationForm(forms.ModelForm):
             self.fields['contacts'].queryset = user.contact_set.filter(
                 verified=True)
 
+        # Sort the queryset for labels by name
+        self.fields['labels'].queryset = \
+            self.fields['labels'].queryset.order_by('name')
+
     def clean(self):
         cleaned_data = super(BaseNotificationForm, self).clean()
 
@@ -71,7 +99,8 @@ class BaseNotificationForm(forms.ModelForm):
         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:
+        # No label specified in the form looks like an empty queryset here
+        if label_query is not None and (labels is not None and labels.exists()):
             err_msg = ('Cannot specify both labels and label query, '
                 'choose one or the other.')
             err_dict[NON_FIELD_ERRORS].append(err_msg)
@@ -93,6 +122,19 @@ class BaseNotificationForm(forms.ModelForm):
 
         return cleaned_data
 
+    def clean_far_threshold(self):
+        far_threshold = self.cleaned_data['far_threshold']
+
+        # If it's set, make sure it's positive
+        if far_threshold is not None:
+            # We can assume it's a float due to previous
+            # validation/cleaning
+            if (far_threshold <= 0):
+                raise forms.ValidationError(
+                    'FAR threshold must be a positive number.')
+
+        return far_threshold
+
 
 class SupereventNotificationForm(BaseNotificationForm, MultipleForm):
     key = 'superevent'
@@ -108,15 +150,40 @@ class EventNotificationForm(BaseNotificationForm, MultipleForm):
     category = Notification.NOTIFICATION_CATEGORY_EVENT
     # Remove 'Test' group
     groups = forms.ModelMultipleChoiceField(queryset=
-        Group.objects.exclude(name='Test'))
+        Group.objects.exclude(name='Test').order_by('name'), required=False,
+        help_text=("Require that the analysis group for the candidate is "
+        "this group or in this set of groups. Leave blank to place no "
+        "requirement on group."))
     # Remove 'MDC' and 'O2VirgoTest' searches
     searches = forms.ModelMultipleChoiceField(queryset=
-        Search.objects.exclude(name__in=['MDC', 'O2VirgoTest']))
+        Search.objects.exclude(name__in=['MDC', 'O2VirgoTest']).order_by('name'),
+        required=False, widget=forms.widgets.SelectMultiple(attrs={'size': 6}),
+        help_text=("Require that the analysis search for the candidate is "
+        "this search or in this set of searches. Leave blank to place no "
+        "requirement on search."))
 
     class Meta(BaseNotificationForm.Meta):
         fields = ['description', 'contacts', 'far_threshold', 'groups',
             'pipelines', 'searches', 'labels', 'label_query', 'ns_candidate',
             'key_field']
+        help_texts = BaseNotificationForm.Meta.help_texts
+        help_texts.update({'pipelines': textwrap.dedent("""\
+            Require that the analysis pipeline for the candidate is this
+            pipeline or in this set of pipelines. Leave blank to place no
+            requirement on pipeline.
+            """).rstrip()
+        })
+        widgets = BaseNotificationForm.Meta.widgets
+        widgets.update({
+            'pipelines': forms.widgets.SelectMultiple(attrs={'size': 6}),
+        })
+
+    def __init__(self, *args, **kwargs):
+        super(EventNotificationForm, self).__init__(*args, **kwargs)
+
+        # Sort the queryset for pipelines by name
+        self.fields['pipelines'].queryset = \
+            self.fields['pipelines'].queryset.order_by('name')
 
 
 ###############################################################################
@@ -128,6 +195,13 @@ class PhoneContactForm(forms.ModelForm, MultipleForm):
     class Meta:
         model = Contact
         fields = ['description', 'phone', 'phone_method', 'key_field']
+        labels = {
+            'phone': 'Phone number',
+        }
+        help_texts = {
+            'phone': ('Non-US numbers should include the country code, '
+                'including the preceding \'+\'.'),
+        }
 
     def __init__(self, *args, **kwargs):
         super(PhoneContactForm, self).__init__(*args, **kwargs)
@@ -140,6 +214,9 @@ class EmailContactForm(forms.ModelForm, MultipleForm):
     class Meta:
         model = Contact
         fields = ['description', 'email', 'key_field']
+        labels = {
+            'email': 'Email address',
+        }
 
 
 class VerifyContactForm(forms.ModelForm):
-- 
GitLab