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.

452 lines
12 KiB

9 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. from collections import Counter
  2. from datetime import timedelta as td
  3. from itertools import tee
  4. from django.conf import settings
  5. from django.contrib.auth.decorators import login_required
  6. from django.core.urlresolvers import reverse
  7. from django.db.models import Count
  8. from django.http import HttpResponseBadRequest, HttpResponseForbidden
  9. from django.shortcuts import get_object_or_404, redirect, render
  10. from django.utils import timezone
  11. from django.utils.crypto import get_random_string
  12. from django.utils.six.moves.urllib.parse import urlencode
  13. from hc.api.decorators import uuid_or_400
  14. from hc.api.models import Channel, Check, Ping, DEFAULT_TIMEOUT, DEFAULT_GRACE
  15. from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm,
  16. TimeoutForm)
  17. # from itertools recipes:
  18. def pairwise(iterable):
  19. "s -> (s0,s1), (s1,s2), (s2, s3), ..."
  20. a, b = tee(iterable)
  21. next(b, None)
  22. return zip(a, b)
  23. @login_required
  24. def my_checks(request):
  25. checks = Check.objects.filter(user=request.team.user).order_by("created")
  26. counter = Counter()
  27. down_tags, grace_tags = set(), set()
  28. for check in checks.iterator():
  29. status = check.get_status()
  30. for tag in check.tags_list():
  31. if tag == "":
  32. continue
  33. counter[tag] += 1
  34. if status == "down":
  35. down_tags.add(tag)
  36. elif status == "grace":
  37. grace_tags.add(tag)
  38. ctx = {
  39. "page": "checks",
  40. "checks": checks,
  41. "now": timezone.now(),
  42. "tags": counter.most_common(),
  43. "down_tags": down_tags,
  44. "grace_tags": grace_tags
  45. }
  46. return render(request, "front/my_checks.html", ctx)
  47. def _welcome_check(request):
  48. check = None
  49. if "welcome_code" in request.session:
  50. code = request.session["welcome_code"]
  51. check = Check.objects.filter(code=code).first()
  52. if check is None:
  53. check = Check()
  54. check.save()
  55. request.session["welcome_code"] = str(check.code)
  56. return check
  57. def index(request):
  58. if request.user.is_authenticated():
  59. return redirect("hc-checks")
  60. check = _welcome_check(request)
  61. ctx = {
  62. "page": "welcome",
  63. "check": check,
  64. "ping_url": check.url(),
  65. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None
  66. }
  67. return render(request, "front/welcome.html", ctx)
  68. def docs(request):
  69. check = _welcome_check(request)
  70. ctx = {
  71. "page": "docs",
  72. "section": "home",
  73. "ping_endpoint": settings.PING_ENDPOINT,
  74. "check": check,
  75. "ping_url": check.url()
  76. }
  77. return render(request, "front/docs.html", ctx)
  78. def docs_api(request):
  79. ctx = {
  80. "page": "docs",
  81. "section": "api",
  82. "SITE_ROOT": settings.SITE_ROOT,
  83. "PING_ENDPOINT": settings.PING_ENDPOINT,
  84. "default_timeout": int(DEFAULT_TIMEOUT.total_seconds()),
  85. "default_grace": int(DEFAULT_GRACE.total_seconds())
  86. }
  87. return render(request, "front/docs_api.html", ctx)
  88. def about(request):
  89. return render(request, "front/about.html", {"page": "about"})
  90. @login_required
  91. def add_check(request):
  92. assert request.method == "POST"
  93. check = Check(user=request.team.user)
  94. check.save()
  95. check.assign_all_channels()
  96. return redirect("hc-checks")
  97. @login_required
  98. @uuid_or_400
  99. def update_name(request, code):
  100. assert request.method == "POST"
  101. check = get_object_or_404(Check, code=code)
  102. if check.user_id != request.team.user.id:
  103. return HttpResponseForbidden()
  104. form = NameTagsForm(request.POST)
  105. if form.is_valid():
  106. check.name = form.cleaned_data["name"]
  107. check.tags = form.cleaned_data["tags"]
  108. check.save()
  109. return redirect("hc-checks")
  110. @login_required
  111. @uuid_or_400
  112. def update_timeout(request, code):
  113. assert request.method == "POST"
  114. check = get_object_or_404(Check, code=code)
  115. if check.user != request.team.user:
  116. return HttpResponseForbidden()
  117. form = TimeoutForm(request.POST)
  118. if form.is_valid():
  119. check.timeout = td(seconds=form.cleaned_data["timeout"])
  120. check.grace = td(seconds=form.cleaned_data["grace"])
  121. check.save()
  122. return redirect("hc-checks")
  123. @login_required
  124. @uuid_or_400
  125. def remove_check(request, code):
  126. assert request.method == "POST"
  127. check = get_object_or_404(Check, code=code)
  128. if check.user != request.team.user:
  129. return HttpResponseForbidden()
  130. check.delete()
  131. return redirect("hc-checks")
  132. @login_required
  133. @uuid_or_400
  134. def log(request, code):
  135. check = get_object_or_404(Check, code=code)
  136. if check.user != request.team.user:
  137. return HttpResponseForbidden()
  138. limit = request.team.ping_log_limit
  139. pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
  140. pings = list(pings.iterator())
  141. # oldest-to-newest order will be more convenient for adding
  142. # "not received" placeholders:
  143. pings.reverse()
  144. # Add a dummy ping object at the end. We iterate over *pairs* of pings
  145. # and don't want to handle a special case of a check with a single ping.
  146. pings.append(Ping(created=timezone.now()))
  147. # Now go through pings, calculate time gaps, and decorate
  148. # the pings list for convenient use in template
  149. wrapped = []
  150. early = False
  151. for older, newer in pairwise(pings):
  152. wrapped.append({"ping": older, "early": early})
  153. # Fill in "missed ping" placeholders:
  154. expected_date = older.created + check.timeout
  155. n_blanks = 0
  156. while expected_date + check.grace < newer.created and n_blanks < 10:
  157. wrapped.append({"placeholder_date": expected_date})
  158. expected_date = expected_date + check.timeout
  159. n_blanks += 1
  160. # Prepare early flag for next ping to come
  161. early = older.created + check.timeout > newer.created + check.grace
  162. reached_limit = len(pings) > limit
  163. wrapped.reverse()
  164. ctx = {
  165. "check": check,
  166. "pings": wrapped,
  167. "num_pings": len(pings),
  168. "limit": limit,
  169. "show_limit_notice": reached_limit and settings.USE_PAYMENTS
  170. }
  171. return render(request, "front/log.html", ctx)
  172. @login_required
  173. def channels(request):
  174. if request.method == "POST":
  175. code = request.POST["channel"]
  176. try:
  177. channel = Channel.objects.get(code=code)
  178. except Channel.DoesNotExist:
  179. return HttpResponseBadRequest()
  180. if channel.user_id != request.team.user.id:
  181. return HttpResponseForbidden()
  182. new_checks = []
  183. for key in request.POST:
  184. if key.startswith("check-"):
  185. code = key[6:]
  186. try:
  187. check = Check.objects.get(code=code)
  188. except Check.DoesNotExist:
  189. return HttpResponseBadRequest()
  190. if check.user_id != request.team.user.id:
  191. return HttpResponseForbidden()
  192. new_checks.append(check)
  193. channel.checks = new_checks
  194. return redirect("hc-channels")
  195. channels = Channel.objects.filter(user=request.team.user).order_by("created")
  196. channels = channels.annotate(n_checks=Count("checks"))
  197. num_checks = Check.objects.filter(user=request.team.user).count()
  198. ctx = {
  199. "page": "channels",
  200. "channels": channels,
  201. "num_checks": num_checks,
  202. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  203. }
  204. return render(request, "front/channels.html", ctx)
  205. def do_add_channel(request, data):
  206. form = AddChannelForm(data)
  207. if form.is_valid():
  208. channel = form.save(commit=False)
  209. channel.user = request.team.user
  210. channel.save()
  211. channel.assign_all_checks()
  212. if channel.kind == "email":
  213. channel.send_verify_link()
  214. return redirect("hc-channels")
  215. else:
  216. return HttpResponseBadRequest()
  217. @login_required
  218. def add_channel(request):
  219. assert request.method == "POST"
  220. return do_add_channel(request, request.POST)
  221. @login_required
  222. @uuid_or_400
  223. def channel_checks(request, code):
  224. channel = get_object_or_404(Channel, code=code)
  225. if channel.user_id != request.team.user.id:
  226. return HttpResponseForbidden()
  227. assigned = set(channel.checks.values_list('code', flat=True).distinct())
  228. checks = Check.objects.filter(user=request.team.user).order_by("created")
  229. ctx = {
  230. "checks": checks,
  231. "assigned": assigned,
  232. "channel": channel
  233. }
  234. return render(request, "front/channel_checks.html", ctx)
  235. @uuid_or_400
  236. def verify_email(request, code, token):
  237. channel = get_object_or_404(Channel, code=code)
  238. if channel.make_token() == token:
  239. channel.email_verified = True
  240. channel.save()
  241. return render(request, "front/verify_email_success.html")
  242. return render(request, "bad_link.html")
  243. @login_required
  244. @uuid_or_400
  245. def remove_channel(request, code):
  246. assert request.method == "POST"
  247. # user may refresh the page during POST and cause two deletion attempts
  248. channel = Channel.objects.filter(code=code).first()
  249. if channel:
  250. if channel.user != request.team.user:
  251. return HttpResponseForbidden()
  252. channel.delete()
  253. return redirect("hc-channels")
  254. @login_required
  255. def add_email(request):
  256. ctx = {"page": "channels"}
  257. return render(request, "integrations/add_email.html", ctx)
  258. @login_required
  259. def add_webhook(request):
  260. if request.method == "POST":
  261. form = AddWebhookForm(request.POST)
  262. if form.is_valid():
  263. channel = Channel(user=request.team.user, kind="webhook")
  264. channel.value = form.get_value()
  265. channel.save()
  266. channel.assign_all_checks()
  267. return redirect("hc-channels")
  268. else:
  269. form = AddWebhookForm()
  270. ctx = {"page": "channels", "form": form}
  271. return render(request, "integrations/add_webhook.html", ctx)
  272. @login_required
  273. def add_pd(request):
  274. ctx = {"page": "channels"}
  275. return render(request, "integrations/add_pd.html", ctx)
  276. @login_required
  277. def add_slack(request):
  278. ctx = {"page": "channels"}
  279. return render(request, "integrations/add_slack.html", ctx)
  280. @login_required
  281. def add_hipchat(request):
  282. ctx = {"page": "channels"}
  283. return render(request, "integrations/add_hipchat.html", ctx)
  284. @login_required
  285. def add_pushover(request):
  286. if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
  287. return HttpResponseForbidden()
  288. if request.method == "POST":
  289. # Initiate the subscription
  290. nonce = get_random_string()
  291. request.session["po_nonce"] = nonce
  292. failure_url = settings.SITE_ROOT + reverse("hc-channels")
  293. success_url = settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({
  294. "nonce": nonce,
  295. "prio": request.POST.get("po_priority", "0"),
  296. })
  297. subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({
  298. "success": success_url,
  299. "failure": failure_url,
  300. })
  301. return redirect(subscription_url)
  302. # Handle successful subscriptions
  303. if "pushover_user_key" in request.GET:
  304. if "nonce" not in request.GET or "prio" not in request.GET:
  305. return HttpResponseBadRequest()
  306. # Validate nonce
  307. if request.GET["nonce"] != request.session.get("po_nonce"):
  308. return HttpResponseForbidden()
  309. # Validate priority
  310. if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"):
  311. return HttpResponseBadRequest()
  312. # All looks well--
  313. del request.session["po_nonce"]
  314. if request.GET.get("pushover_unsubscribed") == "1":
  315. # Unsubscription: delete all Pushover channels for this user
  316. Channel.objects.filter(user=request.user, kind="po").delete()
  317. return redirect("hc-channels")
  318. else:
  319. # Subscription
  320. user_key = request.GET["pushover_user_key"]
  321. priority = int(request.GET["prio"])
  322. return do_add_channel(request, {
  323. "kind": "po",
  324. "value": "%s|%d" % (user_key, priority),
  325. })
  326. # Show Integration Settings form
  327. ctx = {
  328. "page": "channels",
  329. "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
  330. "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
  331. }
  332. return render(request, "integrations/add_pushover.html", ctx)
  333. @login_required
  334. def add_victorops(request):
  335. ctx = {"page": "channels"}
  336. return render(request, "integrations/add_victorops.html", ctx)
  337. def privacy(request):
  338. return render(request, "front/privacy.html", {})