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 = {
'new': 'EH761b6a35102737e3d21830a484a98a08',
'label_added': 'EHb596a53b9c92a41950ce1a47335fd834',
'test': 'EH6c0a168b0c6b011047afa1caeb49b241',
'verify': 'EHfaea274d4d87f6ff152ac39fea3a87d4',
}
# Use timezone-aware datetimes internally
......@@ -443,6 +444,8 @@ GUARDIAN_RENDER_403 = True
# See http://django-guardian.readthedocs.io/en/latest/userguide/custom-user-model.html
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.
PASSWORD_EXPIRATION_TIME = timedelta(days=365)
......
from collections import defaultdict
import logging
from pyparsing import ParseException
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.html import conditional_escape
from django.forms.utils import ErrorList
from django.core.exceptions import NON_FIELD_ERRORS
from django.utils.safestring import mark_safe
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 .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):
class TF(forms.ModelForm):
......@@ -117,58 +123,62 @@ def process_errors(err):
return "\n".join(out_errs)
class ContactForm(forms.ModelForm):
# Adjust labels.
desc = forms.CharField(label='Description', required=True)
call_phone = forms.BooleanField(label='Call', initial=False,
required=False)
text_phone = forms.BooleanField(label='Text', initial=False,
required=False)
class PhoneContactForm(forms.ModelForm, MultipleForm):
key = 'phone'
class Meta:
model = Contact
fields = ['desc','email','phone','call_phone','text_phone']
help_texts = {
'phone': 'Prototype service: may not be available in the future.'
}
# Custom generator for table format.
def as_table(self):
row_head = '<tr><th><label for="id_{0}">{1}:</label></th>'
row_err = '<td>{2}'
row_input = '<input id="id_{0}" name="{0}" type="{3}" /></td></tr>'
row_str = row_head + row_err + row_input
table_data = {}
# Build table -----------------------------
# Description/email
for field in ['desc','email']:
table_data[field] = row_str.format(field,self[field].label,
process_errors(self[field].errors),"text")
# Phone number
table_data['phone'] = (row_head + row_err +
'<input id="id_{0}" name="{0}" type="text" />').format(
'phone',self['phone'].label,process_errors(self['phone'].errors))
# Add call/text checkboxes.
table_data['phone'] += '<br />'
table_data['phone'] += ('{1}?<input id="id_{0}" name="{0}"'
'type="checkbox" />&nbsp;&nbsp;'.format('call_phone',
self['call_phone'].label))
table_data['phone'] += ('{1}?<input id="id_{0}" name="{0}"'
'type="checkbox" />'.format('text_phone',self['text_phone'].label))
# Add phone help text.
table_data['phone'] += ('<br /><span class="helptext">{0}</span>'
'</td></tr>\n'.format(self['phone'].help_text))
# Compile table_data dict into a list.
td = [table_data[k] for k in ['desc','email','phone']]
# Add non-field errors to beginning.
nfe = self.non_field_errors()
if nfe:
td.insert(0,'<tr><td colspan="2">' \
+ process_errors(nfe) + '</td></tr>')
return mark_safe('\n'.join(td))
fields = ['description', 'phone', 'phone_method', 'key_field']
def __init__(self, *args, **kwargs):
super(PhoneContactForm, self).__init__(*args, **kwargs)
self.fields['phone_method'].required = True
class EmailContactForm(forms.ModelForm, MultipleForm):
key = 'email'
class Meta:
model = Contact
fields = ['description', 'email', 'key_field']
class VerifyContactForm(forms.ModelForm):
code = forms.CharField(required=True, label='Verification code')
class Meta:
model = Contact
fields = ['code']
def clean(self):
data = super(VerifyContactForm, self).clean()
# Already verified
if self.instance.verified:
raise forms.ValidationError(_('This contact is already verified.'))
if (self.instance.verification_code is None):
raise forms.ValidationError(_('No verification code has been '
'generated. Please request one before attempted to verify '
'this contact.'))
if (timezone.now() > self.instance.verification_expiration):
raise forms.ValidationError(_('This verification code has '
'expired. Please request a new one.'))
return data
def clean_code(self):
code = self.cleaned_data['code']
# Convert to an int
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
import logging
import random
import textwrap
from django.conf import settings
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.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 .fields import PhoneNumberField
from .phone import get_twilio_from
# Set up logger
logger = logging.getLogger(__name__)
# Set up 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)
desc = models.CharField(max_length=20)
email = models.EmailField(blank=True)
phone = PhoneNumberField(blank=True, max_length=255)
# These fields specify whether alert should be a phone
# call or text (or both).
call_phone = models.BooleanField(default=False)
text_phone = models.BooleanField(default=False)
description = models.CharField(max_length=20, 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,
choices=CONTACT_PHONE_METHODS, default=None)
verified = models.BooleanField(default=False, editable=False)
verification_code = models.IntegerField(null=True, editable=False)
verification_expiration = models.DateTimeField(null=True, editable=False)
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):
# Mostly used for preventing creation of bad Contact
......@@ -32,33 +61,87 @@ class Contact(models.Model):
err_dict = defaultdict(list)
# 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):
err_msg = 'Choose "call" or "text" (or both) for phone alerts.'
err_dict['phone'].append(err_msg)
if (self.phone is not None and self.phone_method is None):
err_msg = 'Choose a phone contact method.'
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_dict['phone'].append(err_msg)
# If no e-mail or phone given, raise error.
if not (self.email or self.phone):
err_msg = 'At least one contact method (email, phone) is required.'
# Only one contact method is allowed
if (self.email is not None and self.phone is not None):
err_msg = \
'Only one contact method (email or phone) can be selected.'
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:
raise ValidationError(err_dict)
# Override save method by requiring fully_cleaned objects.
def save(self, *args, **kwargs):
self.full_clean()
super(Contact, self).save()
def generate_verification_code(self):
self.verification_code = random.randint(10**(self.CODE_DIGITS-1),
(10**self.CODE_DIGITS)-1)
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):
"""Prints information about Contact object; useful for debugging."""
print('Contact "{0}" (user {1}):'.format(self.desc,self.user.username))
print('\tE-mail: {0}'.format(self.email))
print('\tPhone: {0} (call={1}, text={2})'.format(self.phone,
self.call_phone, self.text_phone))
info_str = textwrap.dedent("""\
Contact "{description}" (user {username})
E-mail: {email}
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):
......@@ -89,5 +172,5 @@ class Notification(models.Model):
"|".join([a.name for a in self.pipelines.all()]) or "any pipeline",
label_disp,
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 . import views
app_name = 'alerts'
urlpatterns = [
# Base /options/ URL
url(r'^$', views.index, name="userprofile-home"),
# /options/contact/
url(r'^contact/create$', views.createContact,
name="userprofile-create-contact"),
url(r'^contact/delete/(?P<id>[\d]+)$', views.deleteContact,
name="userprofile-delete-contact"),
url(r'^contact/test/(?P<id>[\d]+)$', views.testContact,
name="userprofile-test-contact"),
#url(r'^contact/edit/(?P<id>[\d]+)$', views.editContact,
# name="userprofile-edit-contact"),
# /options/notification/
url(r'^notification/create$', views.create, name="userprofile-create"),
url(r'^$', views.index, name="index"),
# Contacts
url(r'^contact/create/', views.CreateContactView.as_view(),
name='create-contact'),
url(r'^contact/(?P<pk>\d+)/edit/$', views.EditContactView.as_view(),
name="edit-contact"),
url(r'^contact/(?P<pk>\d+)/delete/$', views.DeleteContactView.as_view(),
name="delete-contact"),
url(r'^contact/(?P<pk>\d+)/test/$', views.TestContactView.as_view(),
name="test-contact"),
url(r'^contact/(?P<pk>\d+)/request-code/$',
views.RequestVerificationCodeView.as_view(),
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,
name="userprofile-delete"),
#url(r'^notification/edit/(?P<id>[\d]+)$', views.edit, name="userprofile-edit"),
name="delete-notification"),
#url(r'^notification/edit/(?P<id>[\d]+)$', views.edit,
# name="edit-notification"),
# /options/manage_password
url(r'^manage_password$', views.managePassword,
name="userprofile-manage-password"),
# Manage password
url(r'^manage_password/$', views.managePassword,
name="manage-password"),
]
import phonenumbers
from django.core.exceptions import ValidationError
def validate_phone(value):
# 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.urls import reverse
from django.core.mail import EmailMessage
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
)
from django.template import RequestContext
from django.shortcuts import render
from django.urls import reverse, reverse_lazy
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.db.models import Q
from django.contrib import messages
from django.views.generic.edit import FormView, DeleteView, UpdateView
from django.views.generic.base import ContextMixin
from django.views.generic.detail import SingleObjectMixin, DetailView
from django_twilio.client import twilio_client
import socket
# Set up logger
import logging
log = logging.getLogger(__name__)
from .models import Notification, Contact
from .forms import ContactForm, notificationFormFactory
from alerts.phone import get_twilio_from
from events.permission_utils import internal_user_required, \
lvem_user_required, is_external
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,
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
def index(request):
notifications = Notification.objects.filter(user=request.user)
......@@ -40,6 +49,7 @@ def index(request):
return render(request, 'profile/notifications.html', context=d)
@lvem_user_required
def managePassword(request):
# lvem_user_required only checks for LVEM group membership,
......@@ -76,6 +86,7 @@ def managePassword(request):
return render(request, 'profile/manage_password.html', context=d)
@internal_user_required
def create(request):
"""Create a notification (Notification) via the web interface"""
......@@ -129,7 +140,7 @@ def create(request):
'{e}.').format(n=t.userlessDisplay(), e=e))
t.delete()
return HttpResponseRedirect(reverse(index))
return HttpResponseRedirect(reverse('alerts:index'))
else:
form = notificationFormFactory(user=request.user)
return render(request, 'profile/createNotification.html',
......@@ -151,108 +162,208 @@ def delete(request, id):
messages.info(request,'Notification "{nname}" has been deleted.' \
.format(nname=t.userlessDisplay()))
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.
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
# Create the Contact
c = Contact(
user = request.user,
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})
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
@internal_user_required
def testContact(request, id):
"""Users can test their Contacts through the web interface"""
try:
c = Contact.objects.get(id=id)
except Contact.DoesNotExist:
raise Http404
if request.user != c.user:
return HttpResponseForbidden("Can't test a Contact that isn't yours.")
else:
messages.info(request, 'Testing contact "{0}".'.format(c.desc))
hostname = socket.gethostname()
if c.email:
# Send test e-mail
try:
subject = 'Test of contact "{0}" from {1}' \
.format(c.desc, hostname)
msg = ('This is a test of contact "{0}" from '
'https://{1}.ligo.org.').format(c.desc, hostname)
email = EmailMessage(subject, msg,
from_email=settings.ALERT_EMAIL_FROM, to=[c.email])
email.send()
log.debug('Sent test e-mail to {0}'.format(c.email))
except Exception as e:
messages.error(request, ("Error sending test e-mail to {0}: "
"{1}.").format(c.email, e))
log.exception('Error sending test e-mail to {0}'.format(c.email))
def form_valid(self, form):
# Remove key_field, add user, and save form
if form.cleaned_data.has_key('key_field'):
form.cleaned_data.pop('key_field')
form.instance.user = self.request.user
form.save()
# Generate message and return
messages.info(self.request, 'Created contact "{cname}".'.format(
cname=form.instance.description))
return super(CreateContactView, self).form_valid(form)
email_form_valid = phone_form_valid = form_valid
@method_decorator(internal_user_required, name='dispatch')
class EditContactView(UpdateView):
"""
Edit a contact. Users shouldn't be able to edit the actual email address
or phone number since that would allow them to circumvent the verification
process.
"""
template_name = 'alerts/edit_contact.html'
# Have to provide form_class, but it will be dynamically selected below in
# get_form()
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
if c.call_phone:
twiml_url = settings.TWIML_BASE_URL \
+ 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))
twiml_url = '{base}{twiml_bin}'.format(
base=settings.TWIML_BASE_URL,
twiml_bin=settings.TWIML_BIN['test'])
return HttpResponseRedirect(reverse(index))
# Make call
twilio_client.calls.create(to=self.object.phone, from_=from_,
url=twiml_url, method='GET')
@internal_user_required
def editContact(request, id):
raise Http404
if (self.object.phone_method == Contact.CONTACT_PHONE_TEXT or
self.object.phone_method == Contact.CONTACT_PHONE_BOTH):
twilio_client.messages.create(to=self.object.phone,
from_=from_, body=msg)
@internal_user_required
def deleteContact(request, id):
"""Users can delete their Contacts through the web interface"""
try:
c = Contact.objects.get(id=id)
except Contact.DoesNotExist:
raise Http404
if request.user != c.user:
return HttpResponseForbidden(("You are not authorized to modify "
"another user's Contacts."))
messages.info(request, 'Contact "{cname}" has been deleted.' \
.format(cname=c.desc))
c.delete()
return HttpResponseRedirect(reverse(index))
# Message for web view
messages.info(request, 'Testing contact "{desc}".'.format(
desc=self.object.description))
return HttpResponseRedirect(self.success_url)
@method_decorator(internal_user_required, name='dispatch')
class VerifyContactView(UpdateView):
"""Request a verification code or verify a contact"""
template_name = 'alerts/verify_contact.html'
form_class = VerifyContactForm
success_url = reverse_lazy('alerts: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" %}
{% block title %}Options | Create Contact{% endblock %}
{% block heading %}Create Contact{% endblock %}
{% block pageid %}userprofile{% endblock %}
{% block title %}Options | Edit Contact{% endblock %}
{% block heading %}Edit contact &quot;{{ object.description }}&quot;{% endblock %}
{% block content %}
<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">
<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 @@
{% endif %}
<li id="nav-latest"><a href="{% url "latest" %}">Latest</a></li>
{% 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 %}
<li id="nav-docs"><a href="{% url "home" %}documentation/">Documentation</a></li>
{% if user %}
......
......@@ -4,6 +4,12 @@
{% block heading %}<!-- leave heading blank -->{% endblock %}
{% block pageid %}userprofile{% endblock %}
{% load static %}
{% block headcontents %}
<link rel="stylesheet" href="{% static "css/bootstrap_buttons.css" %}"></script>
{{ block.super }}
{% endblock %}
{% block content %}
{% if messages %}
......@@ -14,55 +20,74 @@
</div>
{% 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 %}
<ul>
<li>
<a href="{% url "userprofile-test-contact" contact.id %}">Test</a>
<a href="{% url "userprofile-delete-contact" contact.id %}">Delete</a>
{% comment %}
This next part is gross but necessary due to Django whitespace messiness.
{% endcomment %}
{{contact.desc}} |
{% if contact.email and contact.call_phone and contact.text_phone %}
Email {{contact.email}}, call/text {{contact.phone}}
{% elif contact.email and contact.call_phone %}
Email {{contact.email}}, call {{contact.phone}}
{% elif contact.email and contact.text_phone %}
Email {{contact.email}}, text {{contact.phone}}
{% elif contact.email %}
Email {{contact.email}}
{% elif contact.call_phone and contact.text_phone %}
<li style="padding-bottom: 5px;">
{% if contact.verified %}
<a class="btn btn-primary" href="{% url "alerts:test-contact" contact.id %}">Test</a>
{% else %}
<button class="btn btn-primary" disabled>Test</button>
{% endif %}
<a class="btn btn-secondary" href="{% url "alerts:edit-contact" contact.id %}">Edit</a>
<a class="btn btn-danger" href="{% url "alerts:delete-contact" contact.id %}">Delete</a>
<b>{{ contact.description }}</b> |
{% if contact.email %}
Email {{ contact.email }}
{% elif contact.phone %}
{% if contact.phone_method == 'B' %}
Call/text {{contact.phone}}
{% elif contact.call_phone %}
Call {{contact.phone}}
{% elif contact.text_phone %}
{% elif contact.phone_method == 'T' %}
Text {{contact.phone}}
{% else %}
ERROR: this contact has no contact method.
{% elif contact.phone_method == 'C' %}
Call {{contact.phone}}
{% endif %}
</li>
</ul>
{% else %}
<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 %}
</ul>
{% endif %}
<a href="{% url "userprofile-create-contact" %}">Create New Contact</a>
<a href="{% url "alerts:create-contact" %}">Create New Contact</a>
<br/><br/>
<h2>Notifications (LVC Users)</h2>
<h2>Notifications</h2>
{% if notifications %}
<ul>
{% for notification in notifications %}
<ul>
<li>
<a href="{% url "userprofile-delete" notification.id %}">Delete</a>
<li>
<a href="{% url "alerts:delete-notification" notification.id %}">Delete</a>
{{ notification.userlessDisplay }}
</li>
</ul>
</li>
{% 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/>
{% endif %}
<h2>Passwords for Scripted Access (LV-EM users)</h2>
<a href="{% url "userprofile-manage-password" %}">Manage Password</a>
{# Show password page only to LV-EM users who are not also internal #}
{# But let the admins (superusers) see it with some additions #}
{% 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 %}
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