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.

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