diff --git a/Procfile b/Procfile index 415e20cb..9609b8d8 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ -release: bash ./pre-release.sh +release: ./manage.py migrate web: gunicorn hc.wsgi:application \ No newline at end of file diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py index ef5fd3e4..8126586c 100644 --- a/hc/accounts/forms.py +++ b/hc/accounts/forms.py @@ -30,5 +30,16 @@ class RemoveTeamMemberForm(forms.Form): email = LowercaseEmailField() +class AssignChecksForm(forms.Form): + email = LowercaseEmailField() + check_code = forms.UUIDField(required=False) + priority = forms.IntegerField(min_value=0, max_value=5) + + +class UnAssignChecksForm(forms.Form): + email = LowercaseEmailField() + check_code = forms.UUIDField(required=False) + + class TeamNameForm(forms.Form): team_name = forms.CharField(max_length=200, required=True) diff --git a/hc/accounts/migrations/0009_merge_20180711_1206.py b/hc/accounts/migrations/0009_merge_20180711_1206.py new file mode 100644 index 00000000..ea9454df --- /dev/null +++ b/hc/accounts/migrations/0009_merge_20180711_1206.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-11 12:06 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0008_auto_20180703_1212'), + ('accounts', '0008_auto_20180703_0928'), + ] + + operations = [ + ] diff --git a/hc/accounts/migrations/0010_merge_20180712_1640.py b/hc/accounts/migrations/0010_merge_20180712_1640.py new file mode 100644 index 00000000..c999aeb3 --- /dev/null +++ b/hc/accounts/migrations/0010_merge_20180712_1640.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-12 16:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0009_merge_20180711_1206'), + ('accounts', '0009_merge_20180704_1359'), + ] + + operations = [ + ] diff --git a/hc/accounts/migrations/0011_merge_20180716_1253.py b/hc/accounts/migrations/0011_merge_20180716_1253.py new file mode 100644 index 00000000..c3186ad8 --- /dev/null +++ b/hc/accounts/migrations/0011_merge_20180716_1253.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-16 12:53 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0010_merge_20180714_0612'), + ('accounts', '0010_merge_20180712_1640'), + ] + + operations = [ + ] diff --git a/hc/accounts/migrations/0012_auto_20180717_0704.py b/hc/accounts/migrations/0012_auto_20180717_0704.py new file mode 100644 index 00000000..92056db1 --- /dev/null +++ b/hc/accounts/migrations/0012_auto_20180717_0704.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-17 07:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0011_merge_20180716_1253'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='report_frequency', + field=models.CharField(default='month', max_length=20), + ), + ] diff --git a/hc/accounts/tests/test_profile.py b/hc/accounts/tests/test_profile.py index 7a90c0b1..2d51fbae 100644 --- a/hc/accounts/tests/test_profile.py +++ b/hc/accounts/tests/test_profile.py @@ -3,7 +3,7 @@ from hc.test import BaseTestCase from hc.accounts.models import Member -from hc.api.models import Check +from hc.api.models import Check, Assigned class ProfileTestCase(BaseTestCase): @@ -203,3 +203,76 @@ def test_it_loads_reports(self): self.client.login(username="alice@example.org", password="password") response = self.client.get(reverse("hc-reports")) self.assertIn(b"Today's Report", response.content) + + def test_it_assigns_check_to_member(self): + """test that a team member can be removed""" + self.check = Check(user=self.alice) + self.check.save() + self.client.login(username="alice@example.org", password="password") + assigned_list = [] + assigned_list.append(self.check.code) + form = {"assign_checks": "1", "email": "bob@example.org", + "check_code": self.check.code, + "priority": 3, "assigned_list": assigned_list} + r = self.client.post("/accounts/profile/", form) + assert r.status_code == 200 + + self.assertEqual(Assigned.objects.count(), 1) + assigned = Assigned.objects.filter(check_assigned=self.check).first() + self.assertEqual(assigned.priority, 3) + + def test_it_assigns_2_checks_to_one_member(self): + """test that a team member can be removed""" + self.check = Check(user=self.alice) + self.check.save() + self.check2 = Check(user=self.alice) + self.check2.save() + self.client.login(username="alice@example.org", password="password") + assigned_list = [] + assigned_list.append(self.check.code) + assigned_list.append(self.check2.code) + form = {"assign_checks": "1", "email": "bob@example.org", + "check_code": self.check.code, + "priority": "3", "assigned_list": assigned_list} + response = self.client.post("/accounts/profile/", form) + assert response.status_code == 200 + + self.assertEqual(Assigned.objects.count(), 2) + + def test_it_unassign_check_from_members(self): + """test that a team member can be removed""" + self.check = Check(user=self.alice) + self.check.save() + self.client.login(username="alice@example.org", password="password") + assigned_list = [] + assigned_list.append(self.check.code) + form = {"assign_checks": "1", "email": "bob@example.org", + "check_code": self.check.code, + "priority": 3, "assigned_list": assigned_list} + self.client.post("/accounts/profile/", form) + self.assertEqual(Assigned.objects.count(), 1) + assigned_empty = [] + form = {"assign_checks": "1", "email": "bob@example.org", + "check_code": self.check.code, + "assigned_list": assigned_empty} + self.client.post("/accounts/profile/", form) + self.assertEqual(Assigned.objects.count(), 0) + + def test_it_doesnt_assign_check_to_same_user_twice(self): + """test that a team member can be removed""" + self.check = Check(user=self.alice) + self.check.save() + self.client.login(username="alice@example.org", password="password") + assigned_list = [] + assigned_list.append(self.check.code) + form = {"assign_checks": "1", "email": "bob@example.org", + "check_code": self.check.code, + "priority": 3, "assigned_list": assigned_list} + self.client.post("/accounts/profile/", form) + self.assertEqual(Assigned.objects.count(), 1) + + form = {"unassign_check": "1", "email": "bob@example.org", + "check_code": self.check.code, + "assigned_list": assigned_list} + self.client.post("/accounts/profile/", form) + self.assertEqual(Assigned.objects.count(), 1) diff --git a/hc/accounts/views.py b/hc/accounts/views.py index cad234d2..57eafad8 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -11,11 +11,14 @@ from django.core import signing from django.http import HttpResponseForbidden, HttpResponseBadRequest from django.shortcuts import redirect, render -from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm, - RemoveTeamMemberForm, ReportSettingsForm, - SetPasswordForm, TeamNameForm) +from hc.accounts.forms import (EmailPasswordForm, + InviteTeamMemberForm, + RemoveTeamMemberForm, + ReportSettingsForm, + SetPasswordForm, + TeamNameForm) from hc.accounts.models import Profile, Member -from hc.api.models import Channel, Check +from hc.api.models import Channel, Check, Assigned from hc.lib.badges import get_badge_url @@ -155,7 +158,6 @@ def profile(request): elif "show_api_key" in request.POST: show_api_key = True elif "update_reports_allowed" in request.POST: - # print(request.POST) form = ReportSettingsForm(request.POST) if form.is_valid(): profile.reports_allowed = form.cleaned_data["reports_allowed"] @@ -191,6 +193,30 @@ def profile(request): user=farewell_user).delete() messages.info(request, "%s removed from team!" % email) + elif "assign_checks" in request.POST: + email_show = request.POST.get('email') + assigned_show = request.POST.getlist('assigned_list') + priority = request.POST.get('priority') + user = User.objects.filter(email=email_show).first() + delete_list = Assigned.objects.filter(user_id=user.id) + for value in delete_list: + if str(value.check_assigned.code) not in assigned_show: + assigned_check = Check.objects.filter( + code=value.check_assigned.code).first() + Assigned.objects.filter(check_assigned=assigned_check, + user_id=user.id).delete() + for check_code in assigned_show: + user = User.objects.filter(email=email_show).first() + assigned_check = Check.objects.filter(code=check_code).first() + minus_list = Assigned.objects.filter( + check_assigned=assigned_check, user_id=user.id) + if len(list(minus_list)) == 0: + assign = Assigned( + check_assigned=assigned_check, + priority=priority, user_id=user.id) + assign.save() + messages.success(request, + "Team member has been assigned to check") elif "set_team_name" in request.POST: if not profile.team_access_allowed: return HttpResponseForbidden() @@ -212,12 +238,27 @@ def profile(request): continue badge_urls.append(get_badge_url(username, tag)) - + checks = Check.objects.filter(user=request.team.user) + assigned = Assigned.objects.all() + assigned_list = [] + + for member in profile.member_set.all(): + for check in checks: + is_assigned = Assigned.objects.filter(check_assigned=check, + user_id=member.user.id).\ + first() + if is_assigned: + assigned_list.append(1) + else: + assigned_list.append(0) ctx = { "page": "profile", "badge_urls": badge_urls, "profile": profile, - "show_api_key": show_api_key + "show_api_key": show_api_key, + "checks": checks, + "assigned": assigned, + "assigned_list": assigned_list } return render(request, "accounts/profile.html", ctx) diff --git a/hc/api/management/commands/sendalerts.py b/hc/api/management/commands/sendalerts.py index 3473ca6e..81c048b3 100644 --- a/hc/api/management/commands/sendalerts.py +++ b/hc/api/management/commands/sendalerts.py @@ -2,7 +2,6 @@ import time from concurrent.futures import ThreadPoolExecutor from django.core.management.base import BaseCommand -from django.db.models import Q from django.db import connection from django.utils import timezone from hc.api.models import Check @@ -19,10 +18,12 @@ def handle_many(self): query = Check.objects.filter(user__isnull=False).select_related("user") - running_checks = running_checks = query.filter( - Q(status="up") | Q(status="down")) + # running_checks = running_checks = query.filter( + # Q(status="up") | Q(status="down")) now = timezone.now() + going_down = query.filter(alert_after__lt=now, status="up") + going_up = query.filter(alert_after__gt=now, status="down") repeat_list_approved = query.filter( alert_after__lt=now, status="down", @@ -47,12 +48,11 @@ def handle_many(self): if repeat_list_approved: checks = ( - list( - running_checks.iterator()) + + list(going_down.iterator()) + list(going_up.iterator()) + list(repeat_list_approved)) else: checks = ( - list(running_checks.iterator()) + + list(going_down.iterator()) + list(going_up.iterator()) + list(repeat_list_approved.iterator())) if not checks: diff --git a/hc/api/migrations/0035_auto_20180705_1350.py b/hc/api/migrations/0035_auto_20180705_1350.py new file mode 100644 index 00000000..1926f0f8 --- /dev/null +++ b/hc/api/migrations/0035_auto_20180705_1350.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-05 13:50 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0034_auto_20180703_1212'), + ] + + operations = [ + migrations.AddField( + model_name='check', + name='shopify', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='check', + name='shopify_api_key', + field=models.CharField(blank=True, max_length=500), + ), + migrations.AddField( + model_name='check', + name='shopify_name', + field=models.CharField(blank=True, max_length=500), + ), + migrations.AddField( + model_name='check', + name='shopify_password', + field=models.CharField(blank=True, max_length=500), + ), + ] diff --git a/hc/api/migrations/0036_merge_20180711_1206.py b/hc/api/migrations/0036_merge_20180711_1206.py new file mode 100644 index 00000000..2b85063e --- /dev/null +++ b/hc/api/migrations/0036_merge_20180711_1206.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-11 12:06 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0035_auto_20180703_0928'), + ('api', '0035_auto_20180705_1350'), + ] + + operations = [ + ] diff --git a/hc/api/migrations/0051_merge_20180716_1254.py b/hc/api/migrations/0051_merge_20180716_1254.py new file mode 100644 index 00000000..2eb4bd7e --- /dev/null +++ b/hc/api/migrations/0051_merge_20180716_1254.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-16 12:54 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0036_merge_20180711_1206'), + ('api', '0050_merge_20180716_0922'), + ] + + operations = [ + ] diff --git a/hc/api/migrations/0052_auto_20180717_0704.py b/hc/api/migrations/0052_auto_20180717_0704.py new file mode 100644 index 00000000..81fbfa64 --- /dev/null +++ b/hc/api/migrations/0052_auto_20180717_0704.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-17 07:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_merge_20180716_1254'), + ] + + operations = [ + migrations.AlterField( + model_name='check', + name='twilio_number', + field=models.TextField(blank=True, + default='+00000000000', null=True), + ), + ] diff --git a/hc/api/migrations/0053_assigned.py b/hc/api/migrations/0053_assigned.py new file mode 100644 index 00000000..c8a816ef --- /dev/null +++ b/hc/api/migrations/0053_assigned.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-17 07:38 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_auto_20180717_0704'), + ] + + operations = [ + migrations.CreateModel( + name='Assigned', + fields=[ + ('id', models.AutoField(auto_created=True, + primary_key=True, + serialize=False, verbose_name='ID')), + ('user_id', models.IntegerField(default=0)), + ('priority', models.IntegerField(default=0)), + ('check_assigned', models.ForeignKey(on_delete=django.db. + models.deletion.CASCADE, + to='api.Check')), + ], + ), + ] diff --git a/hc/api/models.py b/hc/api/models.py index 131990d4..bdf32a57 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -10,7 +10,6 @@ from django.urls import reverse from django.utils import timezone from hc.api import transports -from hc.accounts.models import Profile from hc.lib import emails STATUSES = ( @@ -56,6 +55,10 @@ class Meta: status = models.CharField(max_length=6, choices=STATUSES, default="new") nag_intervals = models.DurationField(default=DEFAULT_NAG_TIME) nag_after_time = models.DateTimeField(null=True, blank=True) + shopify = models.BooleanField(default=False) + shopify_api_key = models.CharField(max_length=500, blank=True) + shopify_password = models.CharField(max_length=500, blank=True) + shopify_name = models.CharField(max_length=500, blank=True) runs_too_often = models.BooleanField(default=False) priority = models.IntegerField(default=1) number_of_nags = models.IntegerField(default=0) @@ -66,7 +69,6 @@ class Meta: def name_then_code(self): if self.name: return self.name - return str(self.code) def url(self): @@ -79,6 +81,7 @@ def email(self): return "%s@%s" % (self.code, settings.PING_EMAIL_DOMAIN) def send_alert(self): + print("I reach ") if self.status not in ("up", "down"): raise NotImplementedError("Unexpected status: %s" % self.status) if self.priority == 3 and self.number_of_nags < 4: @@ -95,17 +98,20 @@ def send_alert(self): errors = [] if self.escalate: # send alert to people on same team - # find members in team of user - profile = Profile.objects.filter(user=self.user) - team_members = Profile.objects.filter(current_team=profile) - # get channels they ascribe to - for member in team_members: - team_member = member.user - channels = Channel.objects.filter(user=team_member) - for channel in channels: - error = channel.notify(self) - if error not in ("", "no-op"): - errors.append((channel, error)) + # send alert to people assigned to the check + assigns = [] + if self.priority == 2: + assigns = self.escalate_priority(9) + elif self.priority == 3: + assigns = self.escalate_priority(3) + if len(assigns) > 0: + for assign in assigns: + user = User.objects.filter(id=assign.user_id).first() + channels = Channel.objects.filter(user=user) + for channel in channels: + error = channel.notify(self) + if error not in ("", "no-op"): + errors.append((channel, error)) else: for channel in self.channel_set.all(): @@ -115,6 +121,25 @@ def send_alert(self): return errors + def escalate_priority(self, n): + assigns = [] + if self.number_of_nags > n and self.number_of_nags < (n + 6): + assigns = Assigned.objects.filter( + check_assigned=self, priority=1) + if len(assigns) == 0: + self.number_of_nags += 4 + self.save() + if self.number_of_nags > (n + 5) and self.number_of_nags < (n + 11): + assigns = Assigned.objects.filter( + check_assigned=self, priority=2) + if len(assigns) == 0: + self.number_of_nags += 4 + self.save() + if self.number_of_nags > (n + 10): + assigns = Assigned.objects.filter( + check_assigned=self, priority=3) + return assigns + def get_status(self): if self.status in ("new", "paused"): return self.status @@ -302,3 +327,9 @@ class Meta: channel = models.ForeignKey(Channel) created = models.DateTimeField(auto_now_add=True) error = models.CharField(max_length=200, blank=True) + + +class Assigned(models.Model): + check_assigned = models.ForeignKey(Check) + user_id = models.IntegerField(default=0) + priority = models.IntegerField(default=0) diff --git a/hc/api/tests/test_protocols.py b/hc/api/tests/test_protocols.py index 541af39a..033cb77f 100644 --- a/hc/api/tests/test_protocols.py +++ b/hc/api/tests/test_protocols.py @@ -1,10 +1,11 @@ from datetime import timedelta from django.utils import timezone -from hc.api.models import Check, Channel +from hc.api.models import Check, Channel, Assigned from hc.test import BaseTestCase from mock import patch from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import User def fake_twilio_notify(): @@ -31,6 +32,9 @@ def test_it_handles_protocol_list(self): check.number_of_nags = 5 check.priority = 3 check.save() + user = User.objects.filter(email="bob@example.org").first() + assign = Assigned(check_assigned=check, priority=1, user_id=user.id) + assign.save() check.send_alert() assert check.escalate @@ -63,5 +67,26 @@ def test_it_handles_many_checks_protocol_list(self): check2.send_alert() assert check1.escalate - assert check1.number_of_nags == 16 + assert check1.number_of_nags == 20 assert check2.number_of_nags == 6 + + @patch("hc.api.transports.TwilioSms.notify", fake_twilio_notify()) + def test_it_escalates_to_different_members_priorities_if_empty(self): + """ + test it escalates + """ + channel = Channel(user=self.bob, kind="email", + value="bob@example.org") + channel.save() + check = Check(user=self.alice, status="down") + check.last_ping = timezone.now() - timedelta(minutes=300) + check.number_of_nags = 10 + check.priority = 2 + check.save() + user = User.objects.filter(email="bob@example.org").first() + assign = Assigned(check_assigned=check, priority=2, user_id=user.id) + assign.save() + check.send_alert() + + assert check.escalate + assert check.number_of_nags == 15 diff --git a/hc/api/tests/test_sendalerts.py b/hc/api/tests/test_sendalerts.py index 59fe2ca7..97458baa 100644 --- a/hc/api/tests/test_sendalerts.py +++ b/hc/api/tests/test_sendalerts.py @@ -45,4 +45,4 @@ def test_it_handles_grace_period(self, mock): # Assert when Command's handle many that when handle_many should return # True result = Command().handle_many() - assert result, True \ No newline at end of file + assert result, True diff --git a/hc/api/tests/test_sendalerts_until_resolved.py b/hc/api/tests/test_sendalerts_until_resolved.py index a55b2662..ca2da44b 100644 --- a/hc/api/tests/test_sendalerts_until_resolved.py +++ b/hc/api/tests/test_sendalerts_until_resolved.py @@ -23,6 +23,3 @@ def test_it_handles_unresolved(self, mock): result = Command().handle_many() self.assertEqual(result, True) - - # def test_set_priority_level(self) - diff --git a/hc/api/transports.py b/hc/api/transports.py index 0c9e13da..18b6dcf5 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -57,7 +57,6 @@ def notify(self, check): "now": timezone.now(), "show_upgrade_note": show_upgrade_note } - emails.alert(self.channel.value, ctx) diff --git a/hc/api/views.py b/hc/api/views.py index 85ced403..8ed2d1ec 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -37,7 +37,7 @@ def ping(request, code): # Update last ping time and check if it is running too often # i.e. running before the earliest expected ping time check.last_ping = timezone.now() - + check.number_of_nags = 0 if previous_ping: if check.last_ping < allowed_ping_time: check.runs_too_often = True diff --git a/hc/front/forms.py b/hc/front/forms.py index ab3b9a53..47202907 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -22,6 +22,14 @@ class TimeoutForm(forms.Form): grace = forms.IntegerField(min_value=60, max_value=2592000) +class ShopifyForm(forms.Form): + name = forms.CharField(max_length=100, required=False) + api_key = forms.CharField(max_length=100, required=False) + password = forms.CharField(max_length=100, required=False) + event = forms.CharField(max_length=100, required=False) + shop_name = forms.CharField(max_length=100, required=False) + + class NagIntervalForm(forms.Form): nag_interval = forms.IntegerField(min_value=60, max_value=2592000) diff --git a/hc/front/tests/test_add_shopify.py b/hc/front/tests/test_add_shopify.py new file mode 100644 index 00000000..f218ee55 --- /dev/null +++ b/hc/front/tests/test_add_shopify.py @@ -0,0 +1,83 @@ +from hc.test import BaseTestCase +from mock import patch + + +class AddShopifyAlertTestCase(BaseTestCase): + """This class contains tests to handle adding checks""" + + def setUp(self): + super(AddShopifyAlertTestCase, self).setUp() + self.api_key = "84895nfjdufer0n5jnru553jdmfi9" + self.password = "d602f072d117438yjfjfjfu9582ce3" + self.event = "order/create" + self.name = "Create Order" + self.shop_name = "Duuka1" + + def test_it_redirects_add_shopify(self): + """test it renders add_shopify """ + + self.client.login(username="alice@example.org", password="password") + response = self.client.get("/integrations/add_shopify/") + + assert response.status_code == 200 + + @patch('hc.front.views.shopify') + def test_it_accepts_connection_to_shopify(self, mock): + form = {"api_key": self.api_key, + "password": self.password, + "event": self.event, + "name": self.name, + "shop_name": self.shop_name + } + url = "https://%s:%s@%s.myshopify.com/admin" % ( + self.api_key, self.password, self.shop_name) + self.client.login(username="alice@example.org", password="password") + response = self.client.post( + "/checks/create_shopify_alert/", form) + mock.ShopifyResource.set_site.assert_called_with(url) + self.assertEqual(response.status_code, 302) + + @patch('hc.front.views.shopify.ShopifyResource') + @patch('hc.front.views.shopify') + def test_it_doesnot_accept_wrong_details(self, mock, mock_hook): + form = {"api_key": self.api_key, + "password": self.password, + "event": self.event, + "name": self.name, + "shop_name": self.shop_name + } + mock_hook.set_site.side_effect = Exception + self.client.login(username="alice@example.org", password="password") + response = self.client.post( + "/checks/create_shopify_alert/", form) + self.assertEqual(response.status_code, 403) + + @patch('hc.front.views.shopify.Webhook') + @patch('hc.front.views.shopify') + def est_it_creates_alert_and_redirects(self, mock, mock_hook): + form = {"api_key": self.api_key, + "password": self.password, + "event": self.event, + "name": self.name, + "shop_name": self.shop_name + } + mock_hook.find.return_value = [] + self.client.login(username="alice@example.org", password="password") + response = self.client.post( + "/checks/create_shopify_alert/", form) + self.assertEqual(response.status_code, 302) + + @patch('hc.front.views.shopify.Webhook') + @patch('hc.front.views.shopify') + def test_doesnt_create_event_twice(self, mock, mock_hook): + form = {"api_key": self.api_key, + "password": self.password, + "event": self.event, + "name": self.name, + "shop_name": self.shop_name + } + mock_hook.find.return_value = [4, 5] + self.client.login(username="alice@example.org", password="password") + response = self.client.post( + "/checks/create_shopify_alert/", form) + self.assertEqual(response.status_code, 400) diff --git a/hc/front/urls.py b/hc/front/urls.py index 9b40e560..f10a4221 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -32,11 +32,14 @@ url(r'^add_twiliosms/$', views.add_twiliosms, name="hc-add-twiliosms"), url(r'^add_twiliovoice/$', views.add_twiliovoice, name="hc-add-twiliovoice"), + url(r'^add_shopify/$', views.add_shopify, name="hc-add-shopify"), ] urlpatterns = [ url(r'^$', views.index, name="hc-index"), url(r'^checks/$', views.my_checks, name="hc-checks"), + url(r'^checks/create_shopify_alert/$', views.create_shopify_alerts, + name="hc-create-shopify-alerts"), url(r'^checks/add/$', views.add_check, name="hc-add-check"), url(r'^checks/([\w-]+)/', include(check_urls)), url(r'^integrations/', include(channel_urls)), diff --git a/hc/front/views.py b/hc/front/views.py index 8da31b68..9a8485e0 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -5,7 +5,6 @@ import requests from django.conf import settings -from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db.models import Count from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden @@ -15,8 +14,16 @@ from django.utils.six.moves.urllib.parse import urlencode from hc.api.decorators import uuid_or_400 from hc.api.models import DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, Ping -from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm, - TimeoutForm, NagIntervalForm, PriorityForm) +from hc.front.forms import (AddChannelForm, + AddWebhookForm, + NameTagsForm, + TimeoutForm, + NagIntervalForm, + ShopifyForm, + PriorityForm) +import shopify + +from django.contrib import messages # from itertools recipes: @@ -207,6 +214,59 @@ def update_nag_interval(request, code): return redirect("hc-checks") +@login_required +def shopify_alerts(request, code): + assert request.method == "POST" + + return redirect("hc-checks") + + +@login_required +def create_shopify_alerts(request): + assert request.method == "POST" + + form = ShopifyForm(request.POST) + if form.is_valid(): + topic = form.cleaned_data["event"] + api_key = form.cleaned_data["api_key"] + password = form.cleaned_data["password"] + shop_name = form.cleaned_data['shop_name'] + shop_url = "https://%s:%s@%s.myshopify.com/admin" % ( + api_key, password, shop_name) + try: + shopify.ShopifyResource.set_site(shop_url) + shopify.Shop.current + webhook = shopify.Webhook() + webhook_list = shopify.Webhook.find(topic=topic) + if len(webhook_list) > 0: + messages.info( + request, "Trying to add alert for event already created.") + return render(request, "integrations/add_shopify.html", + status=400) + webhook.topic = topic + check = Check(user=request.team.user) + check.name = form.cleaned_data["name"] + check.shopify = True + check.shopify_api_key = api_key + check.shopify_password = password + check.shopify_name = shop_name + check.save() + check_created = Check.objects.filter( + name=form.cleaned_data["name"]).first() + webhook.address = check_created.url() + webhook.format = 'json' + webhook.save() + return redirect("hc-checks") + except Exception: + messages.info( + request, "Unauthorized Access. Cannot access shop in Shopify") + return render(request, "integrations/add_shopify.html", status=403) + + messages.info( + request, "Missing/Wrong field types") + return render(request, "integrations/add_shopify.html", status=400) + + @login_required @uuid_or_400 def pause(request, code): @@ -230,6 +290,24 @@ def remove_check(request, code): check = get_object_or_404(Check, code=code) if check.user != request.team.user: return HttpResponseForbidden() + if check.shopify: + try: + api_key = check.shopify_api_key + password = check.shopify_password + shop_name = check.shopify_name + shop_url = "https://%s:%s@%s.myshopify.com/admin" % ( + api_key, password, shop_name) + shopify.ShopifyResource.set_site(shop_url) + shopify.Shop.current + webhook = shopify.Webhook.find() + for hook in webhook: + if hook.address == check.url(): + hook.destroy() + except Exception: + messages.info( + request, "Unauthorized Access. Cannot access shop in Shopify\ + to delete Webhook") + return redirect("hc-checks") check.delete() @@ -469,6 +547,12 @@ def add_twiliovoice(request): return render(request, "integrations/add_twiliovoice.html", ctx) +@login_required +def add_shopify(request): + ctx = {"page": "channels"} + return render(request, "integrations/add_shopify.html", ctx) + + @login_required def add_slack_btn(request): code = request.GET.get("code", "") diff --git a/hc/settings.py b/hc/settings.py index 50a78509..eb6586bb 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -14,6 +14,8 @@ import dj_database_url from decouple import config import warnings +import dj_database_url +from decouple import config BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -141,14 +143,16 @@ ) COMPRESS_OFFLINE = True -EMAIL_BACKEND = os.environ.get('EMAIL_BACKEND') -EMAIL_HOST = os.environ.get('EMAIL_HOST') +EMAIL_BACKEND = os.environ.get('EMAIL_BACKEND', 'EMAIL_BACKEND') +EMAIL_HOST = os.environ.get('EMAIL_HOST', 'EMAIL_HOST') EMAIL_PORT = 587 -EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') -EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') +EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', 'EMAIL_HOST_USER') +EMAIL_HOST_PASSWORD = os.environ.get( + 'EMAIL_HOST_PASSWORD', 'EMAIL_HOST_PASSWORD') EMAIL_USE_TLS = True -DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL') -SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') +DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', + 'DEFAULT_FROM_EMAIL') +SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY', 'SENDGRID_API_KEY') SENDGRID_SANDBOX_MODE_IN_DEBUG = False # Slack integration -- override these in local_settings diff --git a/requirements.txt b/requirements.txt index 066fb5b8..9031ddeb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,6 +40,7 @@ pyasn1==0.4.3 pycodestyle==2.3.1 pycparser==2.18 pyflakes==1.6.0 +pyactiveresource==2.1.2 PyJWT==1.6.4 pylint==1.9.2 pyOpenSSL==18.0.0 @@ -52,7 +53,15 @@ rcssmin==1.0.6 requests==2.9.1 rjsmin==1.0.12 sendgrid==5.4.1 +PyYAML==3.12 +shopify-trois==1.0 +ShopifyAPI==3.1.0 singledispatch==3.4.0.3 +pytz==2018.5 +PyYAML==3.12 +sendgrid==5.4.1 +shopify-trois==1.0 +ShopifyAPI==3.1.0 six==1.11.0 twilio==6.14.7 whitenoise==3.3.1 diff --git a/static/img/integrations/shopify.jpeg b/static/img/integrations/shopify.jpeg new file mode 100644 index 00000000..78ec8c5b Binary files /dev/null and b/static/img/integrations/shopify.jpeg differ diff --git a/static/img/integrations/shopify_1.jpeg b/static/img/integrations/shopify_1.jpeg new file mode 100644 index 00000000..11fb3937 Binary files /dev/null and b/static/img/integrations/shopify_1.jpeg differ diff --git a/static/js/profile.js b/static/js/profile.js index 74d34433..24db86e1 100644 --- a/static/js/profile.js +++ b/static/js/profile.js @@ -2,11 +2,45 @@ $(function() { $(".member-remove").click(function() { var $this = $(this); - + $("#rtm-email").text($this.data("email")); $("#remove-team-member-email").val($this.data("email")); $('#remove-team-member-modal').modal("show"); + return false; + }); + + $(".assign-checks").click(function () { + var $this = $(this); + var checks_count = $this.data("checks") + var forloop_count = $this.data("count") + + var start = forloop_count * checks_count + var end = ((forloop_count + 1) * checks_count) + var assign_list = $this.data("checksx") + console.log(assign_list) + var compare = [] + for (var i = start; i < end; i++){ + compare.push(assign_list[i]) + } + console.log(compare) + function applyFilters(index, element) { + if (compare[index] == 1){ + $(element).find("input.assigned_test").prop("checked", true); + }else{ + $(element).find("input.assigned_test").prop("checked", false); + } + } + + // Desktop: for each row, see if it needs to be shown or hidden + $(".assign td.loads2").each(applyFilters); + + $("#rtm-email").text($this.data("email")); + $(".remove-team-member-email").val($this.data("email")); + $('#assign-check-modal').modal("show"); + + + return false; }); diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index 7f3a9b4f..048c19d0 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -83,11 +83,20 @@

Team Access

{{ profile.user.email }} Owner + {% for member in profile.member_set.all %} {{ member.user.email }} Member + + Assign Checks + Revoke API Key + +