You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

441 lines
12 KiB

from collections import Counter
from datetime import timedelta as td
from itertools import tee
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.six.moves.urllib.parse import urlencode
from hc.accounts.models import Profile
from hc.api.decorators import uuid_or_400
from hc.api.models import Channel, Check, Ping
from hc.front.forms import AddChannelForm, NameTagsForm, TimeoutForm
# from itertools recipes:
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = tee(iterable)
next(b, None)
return zip(a, b)
@login_required
def my_checks(request):
checks = Check.objects.filter(user=request.user).order_by("created")
counter = Counter()
down_tags, grace_tags = set(), set()
for check in checks.iterator():
status = check.get_status()
for tag in check.tags_list():
if tag == "":
continue
counter[tag] += 1
if status == "down":
down_tags.add(tag)
elif status == "grace":
grace_tags.add(tag)
ctx = {
"page": "checks",
"checks": checks,
"now": timezone.now(),
"tags": counter.most_common(),
"down_tags": down_tags,
"grace_tags": grace_tags
}
return render(request, "front/my_checks.html", ctx)
def _welcome_check(request):
check = None
if "welcome_code" in request.session:
code = request.session["welcome_code"]
check = Check.objects.filter(code=code).first()
if check is None:
check = Check()
check.save()
request.session["welcome_code"] = str(check.code)
return check
def index(request):
if request.user.is_authenticated():
return redirect("hc-checks")
check = _welcome_check(request)
ctx = {
"page": "welcome",
"check": check,
"ping_url": check.url()
}
return render(request, "front/welcome.html", ctx)
def docs(request):
check = _welcome_check(request)
ctx = {
"page": "docs",
"ping_endpoint": settings.PING_ENDPOINT,
"check": check,
"ping_url": check.url()
}
return render(request, "front/docs.html", ctx)
def about(request):
return render(request, "front/about.html", {"page": "about"})
@login_required
def add_check(request):
assert request.method == "POST"
check = Check(user=request.user)
check.save()
check.assign_all_channels()
return redirect("hc-checks")
@login_required
@uuid_or_400
def update_name(request, code):
assert request.method == "POST"
check = get_object_or_404(Check, code=code)
if check.user_id != request.user.id:
return HttpResponseForbidden()
form = NameTagsForm(request.POST)
if form.is_valid():
check.name = form.cleaned_data["name"]
check.tags = form.cleaned_data["tags"]
check.save()
return redirect("hc-checks")
@login_required
@uuid_or_400
def update_timeout(request, code):
assert request.method == "POST"
check = get_object_or_404(Check, code=code)
if check.user != request.user:
return HttpResponseForbidden()
form = TimeoutForm(request.POST)
if form.is_valid():
check.timeout = td(seconds=form.cleaned_data["timeout"])
check.grace = td(seconds=form.cleaned_data["grace"])
check.save()
return redirect("hc-checks")
@login_required
@uuid_or_400
def email_preview(request, code):
""" A debug view to see how email will look.
Will keep it around until I'm happy with email stying.
"""
check = Check.objects.get(code=code)
if check.user != request.user:
return HttpResponseForbidden()
ctx = {
"check": check,
"checks": check.user.check_set.all(),
"now": timezone.now()
}
return render(request, "emails/alert/body.html", ctx)
@login_required
@uuid_or_400
def remove_check(request, code):
assert request.method == "POST"
check = get_object_or_404(Check, code=code)
if check.user != request.user:
return HttpResponseForbidden()
check.delete()
return redirect("hc-checks")
@login_required
@uuid_or_400
def log(request, code):
check = get_object_or_404(Check, code=code)
if check.user != request.user:
return HttpResponseForbidden()
profile = Profile.objects.for_user(request.user)
limit = profile.ping_log_limit
pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
pings = list(pings.iterator())
# oldest-to-newest order will be more convenient for adding
# "not received" placeholders:
pings.reverse()
# Add a dummy ping object at the end. We iterate over *pairs* of pings
# and don't want to handle a special case of a check with a single ping.
pings.append(Ping(created=timezone.now()))
# Now go through pings, calculate time gaps, and decorate
# the pings list for convenient use in template
wrapped = []
early = False
for older, newer in pairwise(pings):
wrapped.append({"ping": older, "early": early})
# Fill in "missed ping" placeholders:
expected_date = older.created + check.timeout
n_blanks = 0
while expected_date + check.grace < newer.created and n_blanks < 10:
wrapped.append({"placeholder_date": expected_date})
expected_date = expected_date + check.timeout
n_blanks += 1
# Prepare early flag for next ping to come
early = older.created + check.timeout > newer.created + check.grace
reached_limit = len(pings) > limit
wrapped.reverse()
ctx = {
"check": check,
"pings": wrapped,
"num_pings": len(pings),
"limit": limit,
"show_limit_notice": reached_limit and settings.USE_PAYMENTS
}
return render(request, "front/log.html", ctx)
@login_required
def channels(request):
if request.method == "POST":
code = request.POST["channel"]
try:
channel = Channel.objects.get(code=code)
except Channel.DoesNotExist:
return HttpResponseBadRequest()
if channel.user_id != request.user.id:
return HttpResponseForbidden()
new_checks = []
for key in request.POST:
if key.startswith("check-"):
code = key[6:]
try:
check = Check.objects.get(code=code)
except Check.DoesNotExist:
return HttpResponseBadRequest()
if check.user_id != request.user.id:
return HttpResponseForbidden()
new_checks.append(check)
channel.checks = new_checks
return redirect("hc-channels")
channels = Channel.objects.filter(user=request.user).order_by("created")
num_checks = Check.objects.filter(user=request.user).count()
ctx = {
"page": "channels",
"channels": channels,
"num_checks": num_checks,
"enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
}
return render(request, "front/channels.html", ctx)
def do_add_channel(request, data):
form = AddChannelForm(data)
if form.is_valid():
channel = form.save(commit=False)
channel.user = request.user
channel.save()
checks = Check.objects.filter(user=request.user)
channel.checks.add(*checks)
if channel.kind == "email":
channel.send_verify_link()
return redirect("hc-channels")
else:
return HttpResponseBadRequest()
@login_required
def add_channel(request):
assert request.method == "POST"
return do_add_channel(request, request.POST)
@login_required
@uuid_or_400
def channel_checks(request, code):
channel = get_object_or_404(Channel, code=code)
if channel.user_id != request.user.id:
return HttpResponseForbidden()
assigned = set(channel.checks.values_list('code', flat=True).distinct())
checks = Check.objects.filter(user=request.user).order_by("created")
ctx = {
"checks": checks,
"assigned": assigned,
"channel": channel
}
return render(request, "front/channel_checks.html", ctx)
@uuid_or_400
def verify_email(request, code, token):
channel = get_object_or_404(Channel, code=code)
if channel.make_token() == token:
channel.email_verified = True
channel.save()
return render(request, "front/verify_email_success.html")
return render(request, "bad_link.html")
@login_required
@uuid_or_400
def remove_channel(request, code):
assert request.method == "POST"
# user may refresh the page during POST and cause two deletion attempts
channel = Channel.objects.filter(code=code).first()
if channel:
if channel.user != request.user:
return HttpResponseForbidden()
channel.delete()
return redirect("hc-channels")
@login_required
def add_email(request):
ctx = {"page": "channels"}
return render(request, "integrations/add_email.html", ctx)
@login_required
def add_webhook(request):
ctx = {"page": "channels"}
return render(request, "integrations/add_webhook.html", ctx)
@login_required
def add_pd(request):
ctx = {"page": "channels"}
return render(request, "integrations/add_pd.html", ctx)
@login_required
def add_slack(request):
ctx = {"page": "channels"}
return render(request, "integrations/add_slack.html", ctx)
@login_required
def add_hipchat(request):
ctx = {"page": "channels"}
return render(request, "integrations/add_hipchat.html", ctx)
@login_required
def add_pushover(request):
if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
return HttpResponseForbidden()
if request.method == "POST":
# Initiate the subscription
nonce = get_random_string()
request.session["po_nonce"] = nonce
failure_url = settings.SITE_ROOT + reverse("hc-channels")
success_url = settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({
"nonce": nonce,
"prio": request.POST.get("po_priority", "0"),
})
subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({
"success": success_url,
"failure": failure_url,
})
return redirect(subscription_url)
# Handle successful subscriptions
if "pushover_user_key" in request.GET:
if "nonce" not in request.GET or "prio" not in request.GET:
return HttpResponseBadRequest()
# Validate nonce
if request.GET["nonce"] != request.session.get("po_nonce"):
return HttpResponseForbidden()
# Validate priority
if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"):
return HttpResponseBadRequest()
# All looks well--
del request.session["po_nonce"]
if request.GET.get("pushover_unsubscribed") == "1":
# Unsubscription: delete all Pushover channels for this user
Channel.objects.filter(user=request.user, kind="po").delete()
return redirect("hc-channels")
else:
# Subscription
user_key = request.GET["pushover_user_key"]
priority = int(request.GET["prio"])
return do_add_channel(request, {
"kind": "po",
"value": "%s|%d" % (user_key, priority),
})
# Show Integration Settings form
ctx = {
"page": "channels",
"po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
"po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
}
return render(request, "integrations/add_pushover.html", ctx)
def privacy(request):
return render(request, "front/privacy.html", {})