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

Overhaul of contacts

Separate phone and email contacts, add verification of contact
information, improve views and forms.
parent d1de4b3b
No related branches found
No related tags found
No related merge requests found
Showing
with 1468 additions and 260 deletions
...@@ -57,6 +57,7 @@ TWIML_BIN = { ...@@ -57,6 +57,7 @@ TWIML_BIN = {
'new': 'EH761b6a35102737e3d21830a484a98a08', 'new': 'EH761b6a35102737e3d21830a484a98a08',
'label_added': 'EHb596a53b9c92a41950ce1a47335fd834', 'label_added': 'EHb596a53b9c92a41950ce1a47335fd834',
'test': 'EH6c0a168b0c6b011047afa1caeb49b241', 'test': 'EH6c0a168b0c6b011047afa1caeb49b241',
'verify': 'EHfaea274d4d87f6ff152ac39fea3a87d4',
} }
# Use timezone-aware datetimes internally # Use timezone-aware datetimes internally
...@@ -443,6 +444,8 @@ GUARDIAN_RENDER_403 = True ...@@ -443,6 +444,8 @@ GUARDIAN_RENDER_403 = True
# See http://django-guardian.readthedocs.io/en/latest/userguide/custom-user-model.html # See http://django-guardian.readthedocs.io/en/latest/userguide/custom-user-model.html
GUARDIAN_MONKEY_PATCH = False GUARDIAN_MONKEY_PATCH = False
# Lifetime of verification codes for contacts
VERIFICATION_CODE_LIFETIME = timedelta(hours=1)
# Basic auth passwords for LVEM scripted access expire after 365 days. # Basic auth passwords for LVEM scripted access expire after 365 days.
PASSWORD_EXPIRATION_TIME = timedelta(days=365) PASSWORD_EXPIRATION_TIME = timedelta(days=365)
......
from collections import defaultdict
import logging
from pyparsing import ParseException
from django import forms from django import forms
from django.utils.safestring import mark_safe from django.core.exceptions import NON_FIELD_ERRORS
from django.forms.utils import ErrorList
from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from django.forms.utils import ErrorList from django.utils.safestring import mark_safe
from django.core.exceptions import NON_FIELD_ERRORS from django.utils.translation import ugettext_lazy as _
from .models import Notification, Contact from core.forms import MultipleForm
from search.query.labels import parseLabelQuery from search.query.labels import parseLabelQuery
from .models import Notification, Contact
# Set up logger
logger = logging.getLogger(__name__)
from pyparsing import ParseException
from collections import defaultdict
import logging
log = logging.getLogger(__name__)
def notificationFormFactory(postdata=None, user=None): def notificationFormFactory(postdata=None, user=None):
class TF(forms.ModelForm): class TF(forms.ModelForm):
...@@ -117,58 +123,62 @@ def process_errors(err): ...@@ -117,58 +123,62 @@ def process_errors(err):
return "\n".join(out_errs) return "\n".join(out_errs)
class ContactForm(forms.ModelForm):
# Adjust labels. class PhoneContactForm(forms.ModelForm, MultipleForm):
desc = forms.CharField(label='Description', required=True) key = 'phone'
call_phone = forms.BooleanField(label='Call', initial=False,
required=False)
text_phone = forms.BooleanField(label='Text', initial=False,
required=False)
class Meta: class Meta:
model = Contact model = Contact
fields = ['desc','email','phone','call_phone','text_phone'] fields = ['description', 'phone', 'phone_method', 'key_field']
help_texts = {
'phone': 'Prototype service: may not be available in the future.' def __init__(self, *args, **kwargs):
} super(PhoneContactForm, self).__init__(*args, **kwargs)
self.fields['phone_method'].required = True
# Custom generator for table format.
def as_table(self):
row_head = '<tr><th><label for="id_{0}">{1}:</label></th>' class EmailContactForm(forms.ModelForm, MultipleForm):
row_err = '<td>{2}' key = 'email'
row_input = '<input id="id_{0}" name="{0}" type="{3}" /></td></tr>'
row_str = row_head + row_err + row_input class Meta:
table_data = {} model = Contact
fields = ['description', 'email', 'key_field']
# Build table -----------------------------
# Description/email
for field in ['desc','email']: class VerifyContactForm(forms.ModelForm):
table_data[field] = row_str.format(field,self[field].label, code = forms.CharField(required=True, label='Verification code')
process_errors(self[field].errors),"text")
class Meta:
# Phone number model = Contact
table_data['phone'] = (row_head + row_err + fields = ['code']
'<input id="id_{0}" name="{0}" type="text" />').format(
'phone',self['phone'].label,process_errors(self['phone'].errors)) def clean(self):
# Add call/text checkboxes. data = super(VerifyContactForm, self).clean()
table_data['phone'] += '<br />'
table_data['phone'] += ('{1}?<input id="id_{0}" name="{0}"' # Already verified
'type="checkbox" />&nbsp;&nbsp;'.format('call_phone', if self.instance.verified:
self['call_phone'].label)) raise forms.ValidationError(_('This contact is already verified.'))
table_data['phone'] += ('{1}?<input id="id_{0}" name="{0}"'
'type="checkbox" />'.format('text_phone',self['text_phone'].label)) if (self.instance.verification_code is None):
raise forms.ValidationError(_('No verification code has been '
# Add phone help text. 'generated. Please request one before attempted to verify '
table_data['phone'] += ('<br /><span class="helptext">{0}</span>' 'this contact.'))
'</td></tr>\n'.format(self['phone'].help_text))
if (timezone.now() > self.instance.verification_expiration):
# Compile table_data dict into a list. raise forms.ValidationError(_('This verification code has '
td = [table_data[k] for k in ['desc','email','phone']] 'expired. Please request a new one.'))
# Add non-field errors to beginning. return data
nfe = self.non_field_errors()
if nfe: def clean_code(self):
td.insert(0,'<tr><td colspan="2">' \ code = self.cleaned_data['code']
+ process_errors(nfe) + '</td></tr>')
# Convert to an int
return mark_safe('\n'.join(td)) try:
code = int(code)
except:
raise forms.ValidationError(_('Incorrect verification code.'))
if (code != self.instance.verification_code):
raise forms.ValidationError(_('Incorrect verification code.'))
return code
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-21 15:57
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='contact',
old_name='desc',
new_name='description',
),
migrations.RemoveField(
model_name='contact',
name='call_phone',
),
migrations.RemoveField(
model_name='contact',
name='text_phone',
),
migrations.AddField(
model_name='contact',
name='phone_method',
field=models.CharField(blank=True, choices=[(b'C', b'Call'), (b'T', b'Text'), (b'B', b'Call and text')], default=None, max_length=1, null=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-21 16:57
from __future__ import unicode_literals
import alerts.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0002_auto_20190121_1557'),
]
operations = [
migrations.AlterField(
model_name='contact',
name='email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AlterField(
model_name='contact',
name='phone',
field=alerts.fields.PhoneNumberField(blank=True, max_length=255, null=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-21 17:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0003_auto_20190121_1657'),
]
operations = [
migrations.AddField(
model_name='contact',
name='verified',
field=models.BooleanField(default=False),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-21 17:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0004_contact_verified'),
]
operations = [
migrations.AlterField(
model_name='contact',
name='verified',
field=models.BooleanField(default=False, editable=False),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-23 21:19
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0005_auto_20190121_1703'),
]
operations = [
migrations.AddField(
model_name='contact',
name='verification_code',
field=models.IntegerField(editable=False, null=True),
),
migrations.AddField(
model_name='contact',
name='verification_requested',
field=models.DateTimeField(editable=False, null=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-23 21:20
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('alerts', '0006_auto_20190123_2119'),
]
operations = [
migrations.RenameField(
model_name='contact',
old_name='verification_requested',
new_name='verification_expires',
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-23 21:20
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('alerts', '0007_auto_20190123_2120'),
]
operations = [
migrations.RenameField(
model_name='contact',
old_name='verification_expires',
new_name='verification_expiration',
),
]
from collections import defaultdict from collections import defaultdict
import logging
import random
import textwrap
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.core.mail import EmailMessage
from django.db import models
from django.utils import timezone
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 events.models import Label, Pipeline
from .fields import PhoneNumberField from .fields import PhoneNumberField
from .phone import get_twilio_from
# Set up logger
logger = logging.getLogger(__name__)
# Set up user model # Set up user model
UserModel = get_user_model() UserModel = get_user_model()
class Contact(models.Model): class Contact(CleanSaveModel):
# Phone contact methods
CONTACT_PHONE_CALL = 'C'
CONTACT_PHONE_TEXT = 'T'
CONTACT_PHONE_BOTH = 'B'
CONTACT_PHONE_METHODS = (
(CONTACT_PHONE_CALL, 'Call'),
(CONTACT_PHONE_TEXT, 'Text'),
(CONTACT_PHONE_BOTH, 'Call and text'),
)
# Number of digits in verification codes
CODE_DIGITS = 6
# Fields
user = models.ForeignKey(UserModel, null=False) user = models.ForeignKey(UserModel, null=False)
desc = models.CharField(max_length=20) description = models.CharField(max_length=20, blank=False, null=False)
email = models.EmailField(blank=True) email = models.EmailField(blank=True, null=True)
phone = PhoneNumberField(blank=True, max_length=255) phone = PhoneNumberField(blank=True, max_length=255, null=True)
# These fields specify whether alert should be a phone phone_method = models.CharField(max_length=1, null=True, blank=True,
# call or text (or both). choices=CONTACT_PHONE_METHODS, default=None)
call_phone = models.BooleanField(default=False) verified = models.BooleanField(default=False, editable=False)
text_phone = models.BooleanField(default=False) verification_code = models.IntegerField(null=True, editable=False)
verification_expiration = models.DateTimeField(null=True, editable=False)
def __unicode__(self): def __unicode__(self):
return u"{0}: {1}".format(self.user.username, self.desc) return u"{0}: {1}".format(self.user.username, self.description)
def clean(self): def clean(self):
# Mostly used for preventing creation of bad Contact # Mostly used for preventing creation of bad Contact
...@@ -32,33 +61,87 @@ class Contact(models.Model): ...@@ -32,33 +61,87 @@ class Contact(models.Model):
err_dict = defaultdict(list) err_dict = defaultdict(list)
# If a phone number is given, require either call or text to be True. # If a phone number is given, require either call or text to be True.
if self.phone and not (self.call_phone or self.text_phone): if (self.phone is not None and self.phone_method is None):
err_msg = 'Choose "call" or "text" (or both) for phone alerts.' err_msg = 'Choose a phone contact method.'
err_dict['phone'].append(err_msg) err_dict['phone_method'].append(err_msg)
if not self.phone and (self.call_phone or self.text_phone): if (self.phone is None and self.phone_method is not None):
err_msg = '"Call" and "text" should be False for non-phone alerts.' err_msg = '"Call" and "text" should be False for non-phone alerts.'
err_dict['phone'].append(err_msg) err_dict['phone'].append(err_msg)
# If no e-mail or phone given, raise error. # Only one contact method is allowed
if not (self.email or self.phone): if (self.email is not None and self.phone is not None):
err_msg = 'At least one contact method (email, phone) is required.' err_msg = \
'Only one contact method (email or phone) can be selected.'
err_dict[NON_FIELD_ERRORS].append(err_msg) err_dict[NON_FIELD_ERRORS].append(err_msg)
# If no e-mail or phone given, raise error.
# We have to skip this due to really annoying behavior with forms.. :(
#if not (self.email or self.phone):
# err_msg = \
# 'One contact method (email or phone) is required.'
# err_dict[NON_FIELD_ERRORS].append(err_msg)
if err_dict: if err_dict:
raise ValidationError(err_dict) raise ValidationError(err_dict)
# Override save method by requiring fully_cleaned objects. def generate_verification_code(self):
def save(self, *args, **kwargs): self.verification_code = random.randint(10**(self.CODE_DIGITS-1),
self.full_clean() (10**self.CODE_DIGITS)-1)
super(Contact, self).save() self.verification_expiration = timezone.now() + \
settings.VERIFICATION_CODE_LIFETIME
self.save(update_fields=['verification_code',
'verification_expiration'])
def send_verification_code(self):
# Message for texts and emails
msg = ('Verification code for contact "{desc}" on {host}: {code}. '
'If you did not request a verification code or do not know what '
'this is, please disregard.').format(desc=self.description,
host=settings.LIGO_FQDN, code=self.verification_code)
if self.email:
subject = 'Verification code for contact "{desc}" on {host}' \
.format(desc=self.description, host=settings.LIGO_FQDN)
email = EmailMessage(subject, msg,
from_email=settings.ALERT_EMAIL_FROM, to=[self.email])
email.send()
elif self.phone:
from_ = get_twilio_from()
if (self.phone_method == self.CONTACT_PHONE_CALL):
# If phone method is only call, send a call
# Convert code to a string with spaces between the numbers
# so it's pronounced properly by text-to-voice
code = " ".join(str(self.verification_code))
urlparams = urlencode({'code': code})
twiml_url = '{base}{twiml_bin}?{params}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['verify'],
params=urlparams)
twilio_client.calls.create(to=self.phone, from_=from_,
url=twiml_url, method='GET')
else:
# If method is text or both, send a text
twilio_client.messages.create(to=self.phone, from_=from_,
body=msg)
def verify(self):
self.verified = True
self.save(update_fields=['verified'])
def print_info(self): def print_info(self):
"""Prints information about Contact object; useful for debugging.""" """Prints information about Contact object; useful for debugging."""
print('Contact "{0}" (user {1}):'.format(self.desc,self.user.username)) info_str = textwrap.dedent("""\
print('\tE-mail: {0}'.format(self.email)) Contact "{description}" (user {username})
print('\tPhone: {0} (call={1}, text={2})'.format(self.phone, E-mail: {email}
self.call_phone, self.text_phone)) Phone: {phone} (method={method})
Verified: {verified}
""").format(description=self.description, username=self.user.username,
email=self.email, phone=self.phone, method=self.phone_method,
verified=self.verified)
print(info_str)
class Notification(models.Model): class Notification(models.Model):
...@@ -89,5 +172,5 @@ class Notification(models.Model): ...@@ -89,5 +172,5 @@ class Notification(models.Model):
"|".join([a.name for a in self.pipelines.all()]) or "any pipeline", "|".join([a.name for a in self.pipelines.all()]) or "any pipeline",
label_disp, label_disp,
thresh, thresh,
", ".join([x.desc for x in self.contacts.all()]) ", ".join([x.description for x in self.contacts.all()])
) )
# Changed for Django 1.11
from django.conf.urls import url from django.conf.urls import url
from . import views from . import views
app_name = 'alerts'
urlpatterns = [ urlpatterns = [
# Base /options/ URL # Base /options/ URL
url(r'^$', views.index, name="userprofile-home"), url(r'^$', views.index, name="index"),
# /options/contact/ # Contacts
url(r'^contact/create$', views.createContact, url(r'^contact/create/', views.CreateContactView.as_view(),
name="userprofile-create-contact"), name='create-contact'),
url(r'^contact/delete/(?P<id>[\d]+)$', views.deleteContact, url(r'^contact/(?P<pk>\d+)/edit/$', views.EditContactView.as_view(),
name="userprofile-delete-contact"), name="edit-contact"),
url(r'^contact/test/(?P<id>[\d]+)$', views.testContact, url(r'^contact/(?P<pk>\d+)/delete/$', views.DeleteContactView.as_view(),
name="userprofile-test-contact"), name="delete-contact"),
#url(r'^contact/edit/(?P<id>[\d]+)$', views.editContact, url(r'^contact/(?P<pk>\d+)/test/$', views.TestContactView.as_view(),
# name="userprofile-edit-contact"), name="test-contact"),
url(r'^contact/(?P<pk>\d+)/request-code/$',
# /options/notification/ views.RequestVerificationCodeView.as_view(),
url(r'^notification/create$', views.create, name="userprofile-create"), name="request-verification-code"),
url(r'^contact/(?P<pk>\d+)/verify/$', views.VerifyContactView.as_view(),
name="verify-contact"),
# Notifications
url(r'^notification/create/$', views.create, name="create-notification"),
url(r'^notification/delete/(?P<id>[\d]+)$', views.delete, url(r'^notification/delete/(?P<id>[\d]+)$', views.delete,
name="userprofile-delete"), name="delete-notification"),
#url(r'^notification/edit/(?P<id>[\d]+)$', views.edit, name="userprofile-edit"), #url(r'^notification/edit/(?P<id>[\d]+)$', views.edit,
# name="edit-notification"),
# /options/manage_password # Manage password
url(r'^manage_password$', views.managePassword, url(r'^manage_password/$', views.managePassword,
name="userprofile-manage-password"), name="manage-password"),
] ]
import phonenumbers import phonenumbers
from django.core.exceptions import ValidationError
def validate_phone(value): def validate_phone(value):
# Try to parse phone number # Try to parse phone number
......
import logging
from django.contrib.auth.decorators import login_required
from django.http import (HttpResponse, HttpResponseRedirect,
HttpResponseNotFound, Http404, HttpResponseForbidden,
HttpResponseBadRequest)
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.contrib import messages
from django.core.mail import EmailMessage from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User 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
)
from django.template import RequestContext from django.template import RequestContext
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.db.models import Q from django.views.generic.edit import FormView, DeleteView, UpdateView
from django.views.generic.base import ContextMixin
from django.contrib import messages from django.views.generic.detail import SingleObjectMixin, DetailView
from django_twilio.client import twilio_client from django_twilio.client import twilio_client
import socket
# Set up logger
import logging
log = logging.getLogger(__name__)
from .models import Notification, Contact from core.views import MultipleFormView
from .forms import ContactForm, notificationFormFactory from events.permission_utils import lvem_user_required, is_external
from alerts.phone import get_twilio_from
from events.permission_utils import internal_user_required, \
lvem_user_required, is_external
from events.models import Label from events.models import Label
from ligoauth.decorators import internal_user_required
from search.query.labels import labelQuery from search.query.labels import labelQuery
from .forms import (
PhoneContactForm, EmailContactForm, VerifyContactForm,
notificationFormFactory,
)
from .models import Notification, Contact
from .phone import get_twilio_from
# Set up logger
logger = log = logging.getLogger(__name__)
# Let's let everybody onto the index view.
#@internal_user_required
@login_required @login_required
def index(request): def index(request):
notifications = Notification.objects.filter(user=request.user) notifications = Notification.objects.filter(user=request.user)
...@@ -40,6 +49,7 @@ def index(request): ...@@ -40,6 +49,7 @@ def index(request):
return render(request, 'profile/notifications.html', context=d) return render(request, 'profile/notifications.html', context=d)
@lvem_user_required @lvem_user_required
def managePassword(request): def managePassword(request):
# lvem_user_required only checks for LVEM group membership, # lvem_user_required only checks for LVEM group membership,
...@@ -76,6 +86,7 @@ def managePassword(request): ...@@ -76,6 +86,7 @@ def managePassword(request):
return render(request, 'profile/manage_password.html', context=d) return render(request, 'profile/manage_password.html', context=d)
@internal_user_required @internal_user_required
def create(request): def create(request):
"""Create a notification (Notification) via the web interface""" """Create a notification (Notification) via the web interface"""
...@@ -129,7 +140,7 @@ def create(request): ...@@ -129,7 +140,7 @@ def create(request):
'{e}.').format(n=t.userlessDisplay(), e=e)) '{e}.').format(n=t.userlessDisplay(), e=e))
t.delete() t.delete()
return HttpResponseRedirect(reverse(index)) return HttpResponseRedirect(reverse('alerts:index'))
else: else:
form = notificationFormFactory(user=request.user) form = notificationFormFactory(user=request.user)
return render(request, 'profile/createNotification.html', return render(request, 'profile/createNotification.html',
...@@ -151,108 +162,208 @@ def delete(request, id): ...@@ -151,108 +162,208 @@ def delete(request, id):
messages.info(request,'Notification "{nname}" has been deleted.' \ messages.info(request,'Notification "{nname}" has been deleted.' \
.format(nname=t.userlessDisplay())) .format(nname=t.userlessDisplay()))
t.delete() t.delete()
return HttpResponseRedirect(reverse(index)) return HttpResponseRedirect(reverse('alerts:index'))
#--------------
#-- Contacts --
#--------------
@internal_user_required ###############################################################################
def createContact(request): # Contact views ###############################################################
###############################################################################
@method_decorator(internal_user_required, name='dispatch')
class CreateContactView(MultipleFormView):
"""Create a contact"""
template_name = 'alerts/create_contact.html'
success_url = reverse_lazy('alerts:index')
form_classes = [PhoneContactForm, EmailContactForm]
# Handle form. def get_context_data(self, **kwargs):
if request.method == "POST": kwargs['idx'] = 0
form = ContactForm(request.POST) if (self.request.method in ('POST', 'PUT')):
if form.is_valid(): form_keys = [f.key for f in self.form_classes]
# Create the Contact idx = form_keys.index(self.request.POST['key_field'])
c = Contact( kwargs['idx'] = idx
user = request.user, return kwargs
desc = form.cleaned_data['desc'],
email = form.cleaned_data['email'],
phone = form.cleaned_data['phone'],
call_phone = form.cleaned_data['call_phone'],
text_phone = form.cleaned_data['text_phone'],
)
c.save()
messages.info(request, 'Created contact "{cname}".'.format(
cname=c.desc))
return HttpResponseRedirect(reverse(index))
else:
form = ContactForm()
return render(request, 'profile/createContact.html',
context={"form": form})
@internal_user_required def form_valid(self, form):
def testContact(request, id):
"""Users can test their Contacts through the web interface""" # Remove key_field, add user, and save form
try: if form.cleaned_data.has_key('key_field'):
c = Contact.objects.get(id=id) form.cleaned_data.pop('key_field')
except Contact.DoesNotExist: form.instance.user = self.request.user
raise Http404 form.save()
if request.user != c.user:
return HttpResponseForbidden("Can't test a Contact that isn't yours.") # Generate message and return
else: messages.info(self.request, 'Created contact "{cname}".'.format(
messages.info(request, 'Testing contact "{0}".'.format(c.desc)) cname=form.instance.description))
hostname = socket.gethostname() return super(CreateContactView, self).form_valid(form)
if c.email:
# Send test e-mail email_form_valid = phone_form_valid = form_valid
try:
subject = 'Test of contact "{0}" from {1}' \
.format(c.desc, hostname) @method_decorator(internal_user_required, name='dispatch')
msg = ('This is a test of contact "{0}" from ' class EditContactView(UpdateView):
'https://{1}.ligo.org.').format(c.desc, hostname) """
email = EmailMessage(subject, msg, Edit a contact. Users shouldn't be able to edit the actual email address
from_email=settings.ALERT_EMAIL_FROM, to=[c.email]) or phone number since that would allow them to circumvent the verification
email.send() process.
log.debug('Sent test e-mail to {0}'.format(c.email)) """
except Exception as e: template_name = 'alerts/edit_contact.html'
messages.error(request, ("Error sending test e-mail to {0}: " # Have to provide form_class, but it will be dynamically selected below in
"{1}.").format(c.email, e)) # get_form()
log.exception('Error sending test e-mail to {0}'.format(c.email)) form_class = PhoneContactForm
success_url = reverse_lazy('alerts:index')
def get_form_class(self):
if self.object.phone is not None:
return PhoneContactForm
else:
return EmailContactForm
return self.form_class
def get_form(self, form_class=None):
form = super(EditContactView, self).get_form(form_class)
if isinstance(form, PhoneContactForm):
form.fields['phone'].disabled = True
elif isinstance(form, EmailContactForm):
form.fields['email'].disabled = True
return form
def get_queryset(self):
return self.request.user.contact_set.all()
@method_decorator(internal_user_required, name='dispatch')
class DeleteContactView(DeleteView):
"""Delete a contact"""
model = Contact
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(DeleteContactView, self).delete(request, *args,
**kwargs)
messages.info(request, 'Contact "{cname}" has been deleted.'.format(
cname=self.object.description))
return response
def get_queryset(self):
# Queryset should only contain the user's contacts
return self.request.user.contact_set.all()
@method_decorator(internal_user_required, name='dispatch')
class TestContactView(DetailView):
"""Test a contact (must be verified already)"""
# Send alerts to all contact methods
model = Contact
success_url = reverse_lazy('alerts:index')
def get_queryset(self):
return self.request.user.contact_set.all()
def get(self, request, *args, **kwargs):
self.object = self.get_object()
# Handle case where contact is not verified
if not self.object.verified:
msg = ('Contact "{desc}" must be verified before it can be '
'tested.').format(desc=self.object.description)
messages.info(request, msg)
return HttpResponseRedirect(self.success_url)
# Send test notifications
msg = 'This is a test of contact "{desc}" from {host}.'.format(
desc=self.object.description, host=settings.LIGO_FQDN)
if self.object.email:
subject = 'Test of contact "{desc}" from {host}'.format(
desc=self.object.description, host=settings.LIGO_FQDN)
email = EmailMessage(subject, msg, settings.ALERT_EMAIL_FROM,
[self.object.email], [])
email.send()
if self.object.phone:
# Get "from" phone number.
from_ = get_twilio_from()
# Send test call
if (self.object.phone_method == Contact.CONTACT_PHONE_CALL or
self.object.phone_method == Contact.CONTACT_PHONE_BOTH):
if c.phone:
# Send test phone alert
try:
# Get "from" phone number.
from_ = get_twilio_from()
# Construct URL of TwiML bin # Construct URL of TwiML bin
if c.call_phone: twiml_url = '{base}{twiml_bin}'.format(
twiml_url = settings.TWIML_BASE_URL \ base=settings.TWIML_BASE_URL,
+ settings.TWIML_BIN['test'] twiml_bin=settings.TWIML_BIN['test'])
twiml_url += "?server={0}".format(hostname)
# Make call
twilio_client.calls.create(to=c.phone, from_=from_,
url=twiml_url, method='GET')
log.debug('Making test call to {0}'.format(c.phone))
if c.text_phone:
twilio_client.messages.create(to=c.phone, from_=from_,
body=('This is a test message from https://{0}'
'.ligo.org.').format(hostname))
log.debug('Sending test text to {0}'.format(c.phone))
except Exception as e:
messages.error(request, "Error contacting {0}: {1}." \
.format(c.phone, e))
log.exception('Error contacting {0}: {1}'.format(c.phone, e))
return HttpResponseRedirect(reverse(index)) # Make call
twilio_client.calls.create(to=self.object.phone, from_=from_,
url=twiml_url, method='GET')
@internal_user_required if (self.object.phone_method == Contact.CONTACT_PHONE_TEXT or
def editContact(request, id): self.object.phone_method == Contact.CONTACT_PHONE_BOTH):
raise Http404
twilio_client.messages.create(to=self.object.phone,
from_=from_, body=msg)
@internal_user_required # Message for web view
def deleteContact(request, id): messages.info(request, 'Testing contact "{desc}".'.format(
"""Users can delete their Contacts through the web interface""" desc=self.object.description))
try:
c = Contact.objects.get(id=id) return HttpResponseRedirect(self.success_url)
except Contact.DoesNotExist:
raise Http404
if request.user != c.user: @method_decorator(internal_user_required, name='dispatch')
return HttpResponseForbidden(("You are not authorized to modify " class VerifyContactView(UpdateView):
"another user's Contacts.")) """Request a verification code or verify a contact"""
messages.info(request, 'Contact "{cname}" has been deleted.' \ template_name = 'alerts/verify_contact.html'
.format(cname=c.desc)) form_class = VerifyContactForm
c.delete() success_url = reverse_lazy('alerts:index')
return HttpResponseRedirect(reverse(index))
def get_queryset(self):
return self.request.user.contact_set.all()
def form_valid(self, form):
self.object.verify()
msg = 'Contact "{cname}" successfully verified.'.format(
cname=self.object.description)
messages.info(self.request, msg)
return super(VerifyContactView, self).form_valid(form)
def get_context_data(self, **kwargs):
context = super(VerifyContactView, self).get_context_data(**kwargs)
# Determine if verification code exists and is expired
if (self.object.verification_code is not None and
timezone.now() > self.object.verification_expiration):
context['code_expired'] = True
return context
@method_decorator(internal_user_required, name='dispatch')
class RequestVerificationCodeView(DetailView):
"""Redirect view for requesting a contact verification code"""
model = Contact
def get_queryset(self):
return self.request.user.contact_set.all()
def get(self, request, *args, **kwargs):
self.object = self.get_object()
# Handle case where contact is already verified
if self.object.verified:
msg = 'Contact "{desc}" is already verified.'.format(
desc=self.object.description)
messages.info(request, msg)
return HttpResponseRedirect(reverse('alerts:index'))
# Otherwise, set up verification code for contact
self.object.generate_verification_code()
# Send verification code
self.object.send_verification_code()
messages.info(request, "Verification code sent.")
return HttpResponseRedirect(reverse('alerts:verify-contact',
args=[self.object.pk]))
/*!
* Bootstrap v4.2.1 (https://getbootstrap.com/)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
button {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button {
overflow: visible;
}
button {
text-transform: none;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
.btn {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1.4rem;
line-height: 1.5;
border-radius: 0.3rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
text-decoration: none;
}
@media screen and (prefers-reduced-motion: reduce) {
.btn {
transition: none;
}
}
.btn:hover {
color: #212529;
text-decoration: none;
}
.btn:focus, .btn.focus {
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn.disabled, .btn:disabled {
opacity: 0.65;
}
.btn:not(:disabled):not(.disabled) {
cursor: pointer;
}
a.btn.disabled,
fieldset:disabled a.btn {
pointer-events: none;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
color: #fff;
background-color: #0069d9;
border-color: #0062cc;
}
.btn-primary:focus, .btn-primary.focus {
box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);
}
.btn-primary.disabled, .btn-primary:disabled {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,
.show > .btn-primary.dropdown-toggle {
color: #fff;
background-color: #0062cc;
border-color: #005cbf;
}
.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,
.show > .btn-primary.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);
}
.btn-secondary {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-secondary:hover {
color: #fff;
background-color: #5a6268;
border-color: #545b62;
}
.btn-secondary:focus, .btn-secondary.focus {
box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);
}
.btn-secondary.disabled, .btn-secondary:disabled {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,
.show > .btn-secondary.dropdown-toggle {
color: #fff;
background-color: #545b62;
border-color: #4e555b;
}
.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,
.show > .btn-secondary.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);
}
.btn-success {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-success:hover {
color: #fff;
background-color: #218838;
border-color: #1e7e34;
}
.btn-success:focus, .btn-success.focus {
box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);
}
.btn-success.disabled, .btn-success:disabled {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,
.show > .btn-success.dropdown-toggle {
color: #fff;
background-color: #1e7e34;
border-color: #1c7430;
}
.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,
.show > .btn-success.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);
}
.btn-info {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8;
}
.btn-info:hover {
color: #fff;
background-color: #138496;
border-color: #117a8b;
}
.btn-info:focus, .btn-info.focus {
box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);
}
.btn-info.disabled, .btn-info:disabled {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8;
}
.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,
.show > .btn-info.dropdown-toggle {
color: #fff;
background-color: #117a8b;
border-color: #10707f;
}
.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,
.show > .btn-info.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);
}
.btn-warning {
color: #212529;
background-color: #ffc107;
border-color: #ffc107;
}
.btn-warning:hover {
color: #212529;
background-color: #e0a800;
border-color: #d39e00;
}
.btn-warning:focus, .btn-warning.focus {
box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);
}
.btn-warning.disabled, .btn-warning:disabled {
color: #212529;
background-color: #ffc107;
border-color: #ffc107;
}
.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,
.show > .btn-warning.dropdown-toggle {
color: #212529;
background-color: #d39e00;
border-color: #c69500;
}
.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,
.show > .btn-warning.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);
}
.btn-danger {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-danger:hover {
color: #fff;
background-color: #c82333;
border-color: #bd2130;
}
.btn-danger:focus, .btn-danger.focus {
box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);
}
.btn-danger.disabled, .btn-danger:disabled {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,
.show > .btn-danger.dropdown-toggle {
color: #fff;
background-color: #bd2130;
border-color: #b21f2d;
}
.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,
.show > .btn-danger.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);
}
.btn-light {
color: #212529;
background-color: #f8f9fa;
border-color: #f8f9fa;
}
.btn-light:hover {
color: #212529;
background-color: #e2e6ea;
border-color: #dae0e5;
}
.btn-light:focus, .btn-light.focus {
box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);
}
.btn-light.disabled, .btn-light:disabled {
color: #212529;
background-color: #f8f9fa;
border-color: #f8f9fa;
}
.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,
.show > .btn-light.dropdown-toggle {
color: #212529;
background-color: #dae0e5;
border-color: #d3d9df;
}
.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,
.show > .btn-light.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);
}
.btn-dark {
color: #fff;
background-color: #343a40;
border-color: #343a40;
}
.btn-dark:hover {
color: #fff;
background-color: #23272b;
border-color: #1d2124;
}
.btn-dark:focus, .btn-dark.focus {
box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
}
.btn-dark.disabled, .btn-dark:disabled {
color: #fff;
background-color: #343a40;
border-color: #343a40;
}
.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,
.show > .btn-dark.dropdown-toggle {
color: #fff;
background-color: #1d2124;
border-color: #171a1d;
}
.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,
.show > .btn-dark.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
}
.btn-outline-primary {
color: #007bff;
border-color: #007bff;
}
.btn-outline-primary:hover {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-outline-primary:focus, .btn-outline-primary.focus {
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);
}
.btn-outline-primary.disabled, .btn-outline-primary:disabled {
color: #007bff;
background-color: transparent;
}
.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,
.show > .btn-outline-primary.dropdown-toggle {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-primary.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:hover {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:focus, .btn-outline-secondary.focus {
box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);
}
.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {
color: #6c757d;
background-color: transparent;
}
.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,
.show > .btn-outline-secondary.dropdown-toggle {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-secondary.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);
}
.btn-outline-success {
color: #28a745;
border-color: #28a745;
}
.btn-outline-success:hover {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-outline-success:focus, .btn-outline-success.focus {
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);
}
.btn-outline-success.disabled, .btn-outline-success:disabled {
color: #28a745;
background-color: transparent;
}
.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,
.show > .btn-outline-success.dropdown-toggle {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-success.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);
}
.btn-outline-info {
color: #17a2b8;
border-color: #17a2b8;
}
.btn-outline-info:hover {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8;
}
.btn-outline-info:focus, .btn-outline-info.focus {
box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);
}
.btn-outline-info.disabled, .btn-outline-info:disabled {
color: #17a2b8;
background-color: transparent;
}
.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,
.show > .btn-outline-info.dropdown-toggle {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8;
}
.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-info.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);
}
.btn-outline-warning {
color: #ffc107;
border-color: #ffc107;
}
.btn-outline-warning:hover {
color: #212529;
background-color: #ffc107;
border-color: #ffc107;
}
.btn-outline-warning:focus, .btn-outline-warning.focus {
box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);
}
.btn-outline-warning.disabled, .btn-outline-warning:disabled {
color: #ffc107;
background-color: transparent;
}
.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,
.show > .btn-outline-warning.dropdown-toggle {
color: #212529;
background-color: #ffc107;
border-color: #ffc107;
}
.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-warning.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);
}
.btn-outline-danger {
color: #dc3545;
border-color: #dc3545;
}
.btn-outline-danger:hover {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-outline-danger:focus, .btn-outline-danger.focus {
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);
}
.btn-outline-danger.disabled, .btn-outline-danger:disabled {
color: #dc3545;
background-color: transparent;
}
.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,
.show > .btn-outline-danger.dropdown-toggle {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-danger.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);
}
.btn-outline-light {
color: #f8f9fa;
border-color: #f8f9fa;
}
.btn-outline-light:hover {
color: #212529;
background-color: #f8f9fa;
border-color: #f8f9fa;
}
.btn-outline-light:focus, .btn-outline-light.focus {
box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);
}
.btn-outline-light.disabled, .btn-outline-light:disabled {
color: #f8f9fa;
background-color: transparent;
}
.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,
.show > .btn-outline-light.dropdown-toggle {
color: #212529;
background-color: #f8f9fa;
border-color: #f8f9fa;
}
.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-light.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);
}
.btn-outline-dark {
color: #343a40;
border-color: #343a40;
}
.btn-outline-dark:hover {
color: #fff;
background-color: #343a40;
border-color: #343a40;
}
.btn-outline-dark:focus, .btn-outline-dark.focus {
box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
}
.btn-outline-dark.disabled, .btn-outline-dark:disabled {
color: #343a40;
background-color: transparent;
}
.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,
.show > .btn-outline-dark.dropdown-toggle {
color: #fff;
background-color: #343a40;
border-color: #343a40;
}
.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-dark.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
}
.btn-link {
font-weight: 400;
color: #007bff;
}
.btn-link:hover {
color: #0056b3;
text-decoration: underline;
}
.btn-link:focus, .btn-link.focus {
text-decoration: underline;
box-shadow: none;
}
.btn-link:disabled, .btn-link.disabled {
color: #6c757d;
pointer-events: none;
}
.btn-lg, .btn-group-lg > .btn {
padding: 0.5rem 1rem;
font-size: 1.25rem;
line-height: 1.5;
border-radius: 0.3rem;
}
.btn-sm, .btn-group-sm > .btn {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
border-radius: 0.2rem;
}
.btn-block {
display: block;
width: 100%;
}
.btn-block + .btn-block {
margin-top: 0.5rem;
}
input[type="submit"].btn-block,
input[type="reset"].btn-block,
input[type="button"].btn-block {
width: 100%;
}
{% 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 Contact{% endblock %}
{% block heading %}Create Contact{% endblock %}
{% block pageid %}userprofile{% endblock %}
{% block content %}
<br />
<div style="padding: 10px;">
<h3>Instructions</h3>
<ul>
<li>Choose an email or phone alert.</li>
<li>A description is required.</li>
<li>For phone alerts, choose call, text, or both.</li>
<li><b>IMPORTANT: after your contact is created, it must be verified before you can receive alerts. You can verify contacts on the previous page where a list of your available contacts is shown.</b></li>
</ul>
</div>
<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>
</div>
{% endfor %}
</div>
{% endblock %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Options | Create Contact{% endblock %} {% block title %}Options | Edit Contact{% endblock %}
{% block heading %}Create Contact{% endblock %} {% block heading %}Edit contact &quot;{{ object.description }}&quot;{% endblock %}
{% block pageid %}userprofile{% endblock %}
{% block content %} {% block content %}
<br /> <br />
<div style="padding: 10px;">
<h3>Instructions</h3>
<ul>
<li>A description of your contact is required.</li>
<li>Choose a contact method (e-mail, phone, or both).</li>
<li>For phone alerts, choose call, text, or both.</li>
</ul>
</div>
<form method="POST"> <form method="POST">
<table> <table>
......
{% extends "base.html" %}
{% load static %}
{% block headcontents %}
<link rel="stylesheet" href="{% static "css/bootstrap_buttons.css" %}"></script>
{{ block.super }}
{% endblock %}
{% block title %}Verify contact{% endblock %}
{% block heading %}
Verify contact &quot;{{ contact.description }}&quot;
{% endblock %}
{% block content %}
<br />
{% if messages %}
<div class="flash">
{% for m in messages %}
<p>{{ m }}</p>
{% endfor %}
</div>
{% endif %}
{% if object.verified %}
This contact is already verified.
{% else %}
<h3>A verification code will be sent via:</h3>
<ul>
<li>Email for email contacts</li>
<li>Text message for phone contacts who have specified &quot;text&quot; or &quot;call and text&quot;</li>
<li>Voice call for phone contacts who have specified &quot;call&quot;</li>
</ul>
<a class="btn btn-primary" href="{% url "alerts:request-verification-code" object.pk %}">
{% if contact.verification_code %}
Request a new verification code
{% else %}
Request a verification code
{% endif %}
</a>
{% if contact.verification_code %}
<br />
<br />
{% if code_expired %}
Your verification code has expired. Use the button above to request a new one.
{% else %}
Your current verification code expires in <b>{{ contact.verification_expiration|timeuntil }}</b>.
<form method="POST">
<table>{{ form.as_table }}</table>
<input type="submit" value="Submit" />
</form>
{% endif %}
{% endif %}
{% endif %}
{% endblock %}
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
{% endif %} {% endif %}
<li id="nav-latest"><a href="{% url "latest" %}">Latest</a></li> <li id="nav-latest"><a href="{% url "latest" %}">Latest</a></li>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li id="nav-userprofile"><a href="{% url "userprofile-home" %}">Options</a></li> <li id="nav-userprofile"><a href="{% url "alerts:index" %}">Options</a></li>
{% endif %} {% endif %}
<li id="nav-docs"><a href="{% url "home" %}documentation/">Documentation</a></li> <li id="nav-docs"><a href="{% url "home" %}documentation/">Documentation</a></li>
{% if user %} {% if user %}
......
...@@ -4,6 +4,12 @@ ...@@ -4,6 +4,12 @@
{% block heading %}<!-- leave heading blank -->{% endblock %} {% block heading %}<!-- leave heading blank -->{% endblock %}
{% block pageid %}userprofile{% endblock %} {% block pageid %}userprofile{% endblock %}
{% load static %}
{% block headcontents %}
<link rel="stylesheet" href="{% static "css/bootstrap_buttons.css" %}"></script>
{{ block.super }}
{% endblock %}
{% block content %} {% block content %}
{% if messages %} {% if messages %}
...@@ -14,55 +20,74 @@ ...@@ -14,55 +20,74 @@
</div> </div>
{% endif %} {% endif %}
<h2>Contacts (LVC Users)</h2> {# Show contacts and notification to internal users only#}
{% if user_is_internal %}
<h2>Contacts</h2>
{% if contacts %}
<ul style="list-style-type: none; padding-left: 10px;">
{% for contact in contacts %} {% for contact in contacts %}
<ul> <li style="padding-bottom: 5px;">
<li> {% if contact.verified %}
<a href="{% url "userprofile-test-contact" contact.id %}">Test</a> <a class="btn btn-primary" href="{% url "alerts:test-contact" contact.id %}">Test</a>
<a href="{% url "userprofile-delete-contact" contact.id %}">Delete</a> {% else %}
{% comment %} <button class="btn btn-primary" disabled>Test</button>
This next part is gross but necessary due to Django whitespace messiness. {% endif %}
{% endcomment %} <a class="btn btn-secondary" href="{% url "alerts:edit-contact" contact.id %}">Edit</a>
{{contact.desc}} | <a class="btn btn-danger" href="{% url "alerts:delete-contact" contact.id %}">Delete</a>
{% if contact.email and contact.call_phone and contact.text_phone %} <b>{{ contact.description }}</b> |
Email {{contact.email}}, call/text {{contact.phone}} {% if contact.email %}
{% elif contact.email and contact.call_phone %} Email {{ contact.email }}
Email {{contact.email}}, call {{contact.phone}} {% elif contact.phone %}
{% elif contact.email and contact.text_phone %} {% if contact.phone_method == 'B' %}
Email {{contact.email}}, text {{contact.phone}}
{% elif contact.email %}
Email {{contact.email}}
{% elif contact.call_phone and contact.text_phone %}
Call/text {{contact.phone}} Call/text {{contact.phone}}
{% elif contact.call_phone %} {% elif contact.phone_method == 'T' %}
Call {{contact.phone}}
{% elif contact.text_phone %}
Text {{contact.phone}} Text {{contact.phone}}
{% else %} {% elif contact.phone_method == 'C' %}
ERROR: this contact has no contact method. Call {{contact.phone}}
{% endif %} {% endif %}
</li> {% else %}
</ul> <b>ERROR: no contact method found.</b>
{% endif %}
<b>
{% if contact.verified %}
<div style="color: green; display: inline-block;">Verified</div>
{% else %}
<div style="color: red; display: inline-block;">Not verified</div>
<a class="btn btn-warning" href="{% url "alerts:verify-contact" contact.id %}">Verify</a>
{% endif %}
</b>
</li>
{% endfor %} {% endfor %}
</ul>
{% endif %}
<a href="{% url "userprofile-create-contact" %}">Create New Contact</a> <a href="{% url "alerts:create-contact" %}">Create New Contact</a>
<br/><br/> <br/><br/>
<h2>Notifications (LVC Users)</h2> <h2>Notifications</h2>
{% if notifications %}
<ul>
{% for notification in notifications %} {% for notification in notifications %}
<ul> <li>
<li> <a href="{% url "alerts:delete-notification" notification.id %}">Delete</a>
<a href="{% url "userprofile-delete" notification.id %}">Delete</a>
{{ notification.userlessDisplay }} {{ notification.userlessDisplay }}
</li> </li>
</ul>
{% endfor %} {% endfor %}
</ul>
{% endif %}
<a href="{% url "userprofile-create" %}">Create New Notification (LVC users)</a> <a href="{% url "alerts:create-notification" %}">Create New Notification</a>
<br/><br/> <br/><br/>
{% endif %}
<h2>Passwords for Scripted Access (LV-EM users)</h2> {# Show password page only to LV-EM users who are not also internal #}
{# But let the admins (superusers) see it with some additions #}
<a href="{% url "userprofile-manage-password" %}">Manage Password</a> {% if user_is_lvem and not user_is_internal or user.is_superuser %}
{% if user.is_superuser %}
<h2 style="border: 2px solid red; color: red; display: inline-block;"><b>The following section is only visible to non-internal LV-EM members. You can see it (and this message) because you are a superuser.</b></h2>
{% endif %}
<h2>Passwords for Scripted Access</h2>
<a href="{% url "alerts:manage-password" %}">Manage Password</a>
{% endif %}
{% endblock %} {% endblock %}
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