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.db.models import Count 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, DEFAULT_TIMEOUT, DEFAULT_GRACE 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(), "enable_pushover": settings.PUSHOVER_API_TOKEN is not None } return render(request, "front/welcome.html", ctx) def docs(request): check = _welcome_check(request) ctx = { "page": "docs", "section": "home", "ping_endpoint": settings.PING_ENDPOINT, "check": check, "ping_url": check.url() } return render(request, "front/docs.html", ctx) def docs_api(request): ctx = { "page": "docs", "section": "api", "SITE_ROOT": settings.SITE_ROOT, "PING_ENDPOINT": settings.PING_ENDPOINT, "default_timeout": int(DEFAULT_TIMEOUT.total_seconds()), "default_grace": int(DEFAULT_GRACE.total_seconds()) } return render(request, "front/docs_api.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") channels = channels.annotate(n_checks=Count("checks")) 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) @login_required def add_victorops(request): ctx = {"page": "channels"} return render(request, "integrations/add_victorops.html", ctx) def privacy(request): return render(request, "front/privacy.html", {})