diff --git a/hc-venv/pip-selfcheck.json b/hc-venv/pip-selfcheck.json new file mode 100644 index 00000000..3379ff89 --- /dev/null +++ b/hc-venv/pip-selfcheck.json @@ -0,0 +1 @@ +{"last_check":"2018-07-12T09:37:21Z","pypi_version":"10.0.1"} \ No newline at end of file diff --git a/hc/accounts/migrations/0008_auto_20180703_1212.py b/hc/accounts/migrations/0008_auto_20180703_1212.py new file mode 100644 index 00000000..64b7a45d --- /dev/null +++ b/hc/accounts/migrations/0008_auto_20180703_1212.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-03 12:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0007_profile_report_frequency'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='report_frequency', + field=models.CharField(default='month', max_length=20), + ), + ] 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/api/migrations/0034_auto_20180703_1212.py b/hc/api/migrations/0034_auto_20180703_1212.py new file mode 100644 index 00000000..df9c5997 --- /dev/null +++ b/hc/api/migrations/0034_auto_20180703_1212.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-07-03 12:12 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0033_merge_20180703_0805'), + ] + + operations = [ + migrations.AlterField( + model_name='check', + name='nag_intervals', + field=models.DurationField(default=datetime.timedelta(1)), + ), + ] 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/models.py b/hc/api/models.py index e5a843f5..10bbf95b 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -56,8 +56,11 @@ 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) - twilio_number = models.TextField(default="+256705357610") + 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) def name_then_code(self): if self.name: diff --git a/hc/front/forms.py b/hc/front/forms.py index 76394aa9..59fe441d 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -18,8 +18,16 @@ def clean_tags(self): class TimeoutForm(forms.Form): - timeout = forms.IntegerField(min_value=60, max_value=2592000) - grace = forms.IntegerField(min_value=60, max_value=2592000) + timeout = forms.IntegerField(min_value=60, max_value=7776000) + grace = forms.IntegerField(min_value=60, max_value=7776000) + + +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): diff --git a/hc/front/tests/test_add_shopify.py b/hc/front/tests/test_add_shopify.py new file mode 100644 index 00000000..5408cf05 --- /dev/null +++ b/hc/front/tests/test_add_shopify.py @@ -0,0 +1,69 @@ +from hc.test import BaseTestCase +from django.core.urlresolvers import reverse +from hc.api.models import Check +import os + + +class AddShopifyAlertTestCase(BaseTestCase): + """This class contains tests to handle adding checks""" + + 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 + + def test_it_accepts_connection_to_shopify(self): + """test it accepts connection """ + API_KEY = os.environ.get('API_KEY') + + PASSWORD = os.environ.get('PASSWORD') + + EVENT = "order/create" + + NAME = "Create Order" + + SHOP_NAME = "Duuka1" + + form = {"api_key": API_KEY, + "password": PASSWORD, + "event": EVENT, + "name": NAME, + "shop_name": SHOP_NAME + } + + url = reverse("hc-create-shopify-alerts") + + self.client.login(username="alice@example.org", password="password") + response = self.client.post(url, form) + + self.assertRedirects(response, "/checks/") + + assert response.status_code == 302 + + def test_it_doesnot_accept_wrong_details(self): + API_KEY = "84895nfjdufer0n5jnru553jdmfi9" + + PASSWORD = "d602f072d117438yjfjfjfu9582ce3" + + EVENT = "order/create" + + NAME = "Create Order" + + SHOP_NAME = "Duuka1" + + form = {"api_key": API_KEY, + "password": PASSWORD, + "event": EVENT, + "name": NAME, + "shop_name": SHOP_NAME + } + + self.client.login(username="alice@example.org", password="password") + response = self.client.post( + "/checks/create_shopify_alert/", form) + + assert response.status_code == 403 + diff --git a/hc/front/urls.py b/hc/front/urls.py index a33ecce7..f04febc9 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -31,11 +31,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 151f2b0d..33d160fb 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -16,7 +16,8 @@ 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) + TimeoutForm, NagIntervalForm, ShopifyForm) +import shopify # from itertools recipes: @@ -185,6 +186,63 @@ 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) + shopify.ShopifyResource.set_site(shop_url) + 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 BaseException: + 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): @@ -208,6 +266,25 @@ 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 BaseException: + messages.info( + request, + "Unauthorized Access. Cannot access shop in Shopify\ + to delete Webhook") + return redirect("hc-checks") check.delete() @@ -447,6 +524,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 4ca8858b..1d847bab 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -151,6 +151,7 @@ SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') SENDGRID_SANDBOX_MODE_IN_DEBUG = False + # Slack integration -- override these in local_settings SLACK_CLIENT_ID = None SLACK_CLIENT_SECRET = None diff --git a/requirements.txt b/requirements.txt index 716f0f34..50caca4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,12 @@ +asn1crypto==0.24.0 astroid==1.6.5 autopep8==1.3.5 backports.functools-lru-cache==1.5 boto==2.48.0 -certifi==2018.4.16 +cffi==1.11.5 coverage==4.5.1 +configparser==3.5.0 +cryptography==2.2.2 cssselect==1.0.3 cssutils==1.0.2 dj-database-url==0.5.0 @@ -13,33 +16,45 @@ django-compressor==2.1 django-sendgrid-v5==0.6.893 django-ses-backend==0.1.1 djmail==0.11.0 +enum34==1.1.6 flake8==3.5.0 +funcsigs==1.0.2 future==0.16.0 futures==3.0.3 gunicorn==19.8.1 +idna==2.7 +ipaddress==1.0.22 isort==4.3.4 lazy-object-proxy==1.3.1 lxml==4.2.1 mccabe==0.6.1 mock==2.0.0 +ndg-httpsclient==0.5.0 pbr==4.0.4 premailer==2.9.6 psycopg2==2.7.5 -pycodestyle==2.3.0 psycopg2-binary==2.7.5 +pyasn1==0.4.3 +pycodestyle==2.3.1 +pycparser==2.18 pyflakes==1.6.0 PyJWT==1.6.4 pylint==1.9.2 +pyOpenSSL==18.0.0 PySocks==1.6.8 python-decouple==3.1 python-http-client==3.1.0 python-telegram-bot==10.1.0 -pytz==2018.4 +pytz==2018.5 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 six==1.11.0 -twilio==6.14.6 +twilio==6.14.7 whitenoise==3.3.1 wrapt==1.10.11 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/checks.js b/static/js/checks.js index e9ef1218..813ea015 100644 --- a/static/js/checks.js +++ b/static/js/checks.js @@ -36,14 +36,16 @@ $(function () { connect: "lower", range: { 'min': [60, 60], - '33%': [3600, 3600], - '66%': [86400, 86400], - '83%': [604800, 604800], - 'max': 2592000, + '16%': [1800, 1800], + '33%': [86400, 86400], + '50%': [604800, 86400], + '66%': [2592000, 86400], + '83%': [5184000, 86400], + 'max': 7776000, }, pips: { mode: 'values', - values: [60, 1800, 3600, 43200, 86400, 604800, 2592000], + values: [60, 1800, 86400, 604800, 2592000, 5184000, 7776000], density: 4, format: { to: secsToText, @@ -96,14 +98,16 @@ $(function () { connect: "lower", range: { 'min': [60, 60], - '33%': [3600, 3600], - '66%': [86400, 86400], - '83%': [604800, 604800], - 'max': 2592000, + '16%': [1800, 1800], + '33%': [86400, 86400], + '50%': [604800, 86400], + '66%': [2592000, 86400], + '83%': [5184000, 86400], + 'max': 7776000, }, pips: { mode: 'values', - values: [60, 1800, 3600, 43200, 86400, 604800, 2592000], + values: [60, 1800, 86400, 604800, 2592000, 5184000, 7776000], density: 4, format: { to: secsToText, diff --git a/templates/front/channels.html b/templates/front/channels.html index 062b1b19..08e11118 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -145,6 +145,13 @@

Sms

Get an sms message when check goes up or down.

Add Integration +
  • + shopify icon + +

    Shopify

    +

    Integrate With the Shopify Application.

    + Add Integration +
  • Voice icon diff --git a/templates/front/my_checks.html b/templates/front/my_checks.html index 4b5e7d97..7ea7c261 100644 --- a/templates/front/my_checks.html +++ b/templates/front/my_checks.html @@ -32,7 +32,15 @@

    - +
    + {% if messages %} +
    + {% for message in messages %} +

    {{ message }}

    + {% endfor %} +
    + {% endif %} +
    {% if checks %} {% include "front/my_checks_mobile.html" %} diff --git a/templates/integrations/add_shopify.html b/templates/integrations/add_shopify.html new file mode 100644 index 00000000..8763e66a --- /dev/null +++ b/templates/integrations/add_shopify.html @@ -0,0 +1,93 @@ + {% extends "base.html" %} + {% load compress humanize staticfiles hc_extras %} + {% block title %}Add Shopify Integration - healthchecks.io{%endblock %} + {% block content %} +
    +
    +

    Shopify

    +
    + {% if messages %} +
    + {% for message in messages %} +

    {{ message }}

    + {% endfor %} +
    + {% endif %} +
    + +

    + Enables create shopify alerts from your shopify application tasks. +

    + +

    Integration Settings

    + + The url for the alert created for shopify will be automatically generated. + +
    + {% csrf_token %} +
    + +
    + + + + NAME OF SHOP IN SHOPIFY + + +
    +
    +
    + +
    + + + + SET THE NAME FOR THE ALERT TO BE CREATED. + + +
    +
    +
    + +
    + + + + GET THE API KEY CREDENTIALS FROM THE PRIVATE APP CREATED IN THE SHOPIFY ADMIN + + +
    +
    +
    + +
    + + + + GET THE PASSWORD CREDENTIALS FROM THE PRIVATE APP CREATED IN THE SHOPIFY ADMIN + + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +{% endblock %} {% block scripts %} {% compress js %} + + {% endcompress %} {% endblock %} \ No newline at end of file