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.

464 lines
13 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
10 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
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
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.accounts.models import Profile
  14. from hc.api.decorators import uuid_or_400
  15. from hc.api.models import Channel, Check, Ping, DEFAULT_TIMEOUT, DEFAULT_GRACE
  16. from hc.front.forms import AddChannelForm, NameTagsForm, 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.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.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.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.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 email_preview(request, code):
  126. """ A debug view to see how email will look.
  127. Will keep it around until I'm happy with email stying.
  128. """
  129. check = Check.objects.get(code=code)
  130. if check.user != request.user:
  131. return HttpResponseForbidden()
  132. ctx = {
  133. "check": check,
  134. "checks": check.user.check_set.all(),
  135. "now": timezone.now()
  136. }
  137. return render(request, "emails/alert/body.html", ctx)
  138. @login_required
  139. @uuid_or_400
  140. def remove_check(request, code):
  141. assert request.method == "POST"
  142. check = get_object_or_404(Check, code=code)
  143. if check.user != request.user:
  144. return HttpResponseForbidden()
  145. check.delete()
  146. return redirect("hc-checks")
  147. @login_required
  148. @uuid_or_400
  149. def log(request, code):
  150. check = get_object_or_404(Check, code=code)
  151. if check.user != request.user:
  152. return HttpResponseForbidden()
  153. profile = Profile.objects.for_user(request.user)
  154. limit = profile.ping_log_limit
  155. pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
  156. pings = list(pings.iterator())
  157. # oldest-to-newest order will be more convenient for adding
  158. # "not received" placeholders:
  159. pings.reverse()
  160. # Add a dummy ping object at the end. We iterate over *pairs* of pings
  161. # and don't want to handle a special case of a check with a single ping.
  162. pings.append(Ping(created=timezone.now()))
  163. # Now go through pings, calculate time gaps, and decorate
  164. # the pings list for convenient use in template
  165. wrapped = []
  166. early = False
  167. for older, newer in pairwise(pings):
  168. wrapped.append({"ping": older, "early": early})
  169. # Fill in "missed ping" placeholders:
  170. expected_date = older.created + check.timeout
  171. n_blanks = 0
  172. while expected_date + check.grace < newer.created and n_blanks < 10:
  173. wrapped.append({"placeholder_date": expected_date})
  174. expected_date = expected_date + check.timeout
  175. n_blanks += 1
  176. # Prepare early flag for next ping to come
  177. early = older.created + check.timeout > newer.created + check.grace
  178. reached_limit = len(pings) > limit
  179. wrapped.reverse()
  180. ctx = {
  181. "check": check,
  182. "pings": wrapped,
  183. "num_pings": len(pings),
  184. "limit": limit,
  185. "show_limit_notice": reached_limit and settings.USE_PAYMENTS
  186. }
  187. return render(request, "front/log.html", ctx)
  188. @login_required
  189. def channels(request):
  190. if request.method == "POST":
  191. code = request.POST["channel"]
  192. try:
  193. channel = Channel.objects.get(code=code)
  194. except Channel.DoesNotExist:
  195. return HttpResponseBadRequest()
  196. if channel.user_id != request.user.id:
  197. return HttpResponseForbidden()
  198. new_checks = []
  199. for key in request.POST:
  200. if key.startswith("check-"):
  201. code = key[6:]
  202. try:
  203. check = Check.objects.get(code=code)
  204. except Check.DoesNotExist:
  205. return HttpResponseBadRequest()
  206. if check.user_id != request.user.id:
  207. return HttpResponseForbidden()
  208. new_checks.append(check)
  209. channel.checks = new_checks
  210. return redirect("hc-channels")
  211. channels = Channel.objects.filter(user=request.user).order_by("created")
  212. channels = channels.annotate(n_checks=Count("checks"))
  213. num_checks = Check.objects.filter(user=request.user).count()
  214. ctx = {
  215. "page": "channels",
  216. "channels": channels,
  217. "num_checks": num_checks,
  218. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  219. }
  220. return render(request, "front/channels.html", ctx)
  221. def do_add_channel(request, data):
  222. form = AddChannelForm(data)
  223. if form.is_valid():
  224. channel = form.save(commit=False)
  225. channel.user = request.user
  226. channel.save()
  227. checks = Check.objects.filter(user=request.user)
  228. channel.checks.add(*checks)
  229. if channel.kind == "email":
  230. channel.send_verify_link()
  231. return redirect("hc-channels")
  232. else:
  233. return HttpResponseBadRequest()
  234. @login_required
  235. def add_channel(request):
  236. assert request.method == "POST"
  237. return do_add_channel(request, request.POST)
  238. @login_required
  239. @uuid_or_400
  240. def channel_checks(request, code):
  241. channel = get_object_or_404(Channel, code=code)
  242. if channel.user_id != request.user.id:
  243. return HttpResponseForbidden()
  244. assigned = set(channel.checks.values_list('code', flat=True).distinct())
  245. checks = Check.objects.filter(user=request.user).order_by("created")
  246. ctx = {
  247. "checks": checks,
  248. "assigned": assigned,
  249. "channel": channel
  250. }
  251. return render(request, "front/channel_checks.html", ctx)
  252. @uuid_or_400
  253. def verify_email(request, code, token):
  254. channel = get_object_or_404(Channel, code=code)
  255. if channel.make_token() == token:
  256. channel.email_verified = True
  257. channel.save()
  258. return render(request, "front/verify_email_success.html")
  259. return render(request, "bad_link.html")
  260. @login_required
  261. @uuid_or_400
  262. def remove_channel(request, code):
  263. assert request.method == "POST"
  264. # user may refresh the page during POST and cause two deletion attempts
  265. channel = Channel.objects.filter(code=code).first()
  266. if channel:
  267. if channel.user != request.user:
  268. return HttpResponseForbidden()
  269. channel.delete()
  270. return redirect("hc-channels")
  271. @login_required
  272. def add_email(request):
  273. ctx = {"page": "channels"}
  274. return render(request, "integrations/add_email.html", ctx)
  275. @login_required
  276. def add_webhook(request):
  277. ctx = {"page": "channels"}
  278. return render(request, "integrations/add_webhook.html", ctx)
  279. @login_required
  280. def add_pd(request):
  281. ctx = {"page": "channels"}
  282. return render(request, "integrations/add_pd.html", ctx)
  283. @login_required
  284. def add_slack(request):
  285. ctx = {"page": "channels"}
  286. return render(request, "integrations/add_slack.html", ctx)
  287. @login_required
  288. def add_hipchat(request):
  289. ctx = {"page": "channels"}
  290. return render(request, "integrations/add_hipchat.html", ctx)
  291. @login_required
  292. def add_pushover(request):
  293. if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
  294. return HttpResponseForbidden()
  295. if request.method == "POST":
  296. # Initiate the subscription
  297. nonce = get_random_string()
  298. request.session["po_nonce"] = nonce
  299. failure_url = settings.SITE_ROOT + reverse("hc-channels")
  300. success_url = settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({
  301. "nonce": nonce,
  302. "prio": request.POST.get("po_priority", "0"),
  303. })
  304. subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({
  305. "success": success_url,
  306. "failure": failure_url,
  307. })
  308. return redirect(subscription_url)
  309. # Handle successful subscriptions
  310. if "pushover_user_key" in request.GET:
  311. if "nonce" not in request.GET or "prio" not in request.GET:
  312. return HttpResponseBadRequest()
  313. # Validate nonce
  314. if request.GET["nonce"] != request.session.get("po_nonce"):
  315. return HttpResponseForbidden()
  316. # Validate priority
  317. if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"):
  318. return HttpResponseBadRequest()
  319. # All looks well--
  320. del request.session["po_nonce"]
  321. if request.GET.get("pushover_unsubscribed") == "1":
  322. # Unsubscription: delete all Pushover channels for this user
  323. Channel.objects.filter(user=request.user, kind="po").delete()
  324. return redirect("hc-channels")
  325. else:
  326. # Subscription
  327. user_key = request.GET["pushover_user_key"]
  328. priority = int(request.GET["prio"])
  329. return do_add_channel(request, {
  330. "kind": "po",
  331. "value": "%s|%d" % (user_key, priority),
  332. })
  333. # Show Integration Settings form
  334. ctx = {
  335. "page": "channels",
  336. "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
  337. "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
  338. }
  339. return render(request, "integrations/add_pushover.html", ctx)
  340. @login_required
  341. def add_victorops(request):
  342. ctx = {"page": "channels"}
  343. return render(request, "integrations/add_victorops.html", ctx)
  344. def privacy(request):
  345. return render(request, "front/privacy.html", {})