forked from GithubBackups/healthchecks
Merge pull request #140 from someposer/master
Adding Content-Type header to Webhook integrations
This commit is contained in:
commit
fbf28e4038
@ -304,22 +304,44 @@ class Channel(models.Model):
|
|||||||
return user_key, prio, PO_PRIORITIES[prio]
|
return user_key, prio, PO_PRIORITIES[prio]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value_down(self):
|
def url_down(self):
|
||||||
assert self.kind == "webhook"
|
assert self.kind == "webhook"
|
||||||
parts = self.value.split("\n")
|
if not self.value.startswith("{"):
|
||||||
return parts[0]
|
parts = self.value.split("\n")
|
||||||
|
return parts[0]
|
||||||
|
|
||||||
|
doc = json.loads(self.value)
|
||||||
|
return doc["url_down"]
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value_up(self):
|
def url_up(self):
|
||||||
assert self.kind == "webhook"
|
assert self.kind == "webhook"
|
||||||
parts = self.value.split("\n")
|
if not self.value.startswith("{"):
|
||||||
return parts[1] if len(parts) > 1 else ""
|
parts = self.value.split("\n")
|
||||||
|
return parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
doc = json.loads(self.value)
|
||||||
|
return doc["url_up"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def post_data(self):
|
def post_data(self):
|
||||||
assert self.kind == "webhook"
|
assert self.kind == "webhook"
|
||||||
parts = self.value.split("\n")
|
if not self.value.startswith("{"):
|
||||||
return parts[2] if len(parts) > 2 else ""
|
parts = self.value.split("\n")
|
||||||
|
return parts[2] if len(parts) > 2 else ""
|
||||||
|
|
||||||
|
doc = json.loads(self.value)
|
||||||
|
return doc["post_data"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self):
|
||||||
|
assert self.kind == "webhook"
|
||||||
|
if not self.value.startswith("{"):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
doc = json.loads(self.value)
|
||||||
|
return doc["headers"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slack_team(self):
|
def slack_team(self):
|
||||||
|
@ -145,6 +145,46 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
# unicode should be encoded into utf-8
|
# unicode should be encoded into utf-8
|
||||||
self.assertTrue(isinstance(kwargs["data"], binary_type))
|
self.assertTrue(isinstance(kwargs["data"], binary_type))
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_webhooks_handle_json_value(self, mock_request):
|
||||||
|
self._setup_data("webhook", '{"url_down": "http://foo.com", '
|
||||||
|
'"url_up": "", "post_data": "", "headers": ""}')
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "healthchecks.io"
|
||||||
|
}
|
||||||
|
mock_request.assert_called_with(
|
||||||
|
"get", "http://foo.com", headers=headers,
|
||||||
|
timeout=5)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_webhooks_handle_json_up_event(self, mock_request):
|
||||||
|
self._setup_data("webhook", '{"url_down": "", '
|
||||||
|
'"url_up": "http://bar", "post_data": "", "headers": ""}', status="up")
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "healthchecks.io"
|
||||||
|
}
|
||||||
|
mock_request.assert_called_with(
|
||||||
|
"get", "http://bar", headers=headers,
|
||||||
|
timeout=5)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_webhooks_handle_headers(self, mock_request):
|
||||||
|
self._setup_data("webhook", '{"url_down": "http://foo.com", '
|
||||||
|
'"url_up": "", "post_data": "data", '
|
||||||
|
'"headers": {"Content-Type": "application/json"}}')
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "healthchecks.io",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
mock_request.assert_called_with(
|
||||||
|
"post", "http://foo.com", data=b"data", headers=headers, timeout=5)
|
||||||
|
|
||||||
def test_email(self):
|
def test_email(self):
|
||||||
self._setup_data("email", "alice@example.org")
|
self._setup_data("email", "alice@example.org")
|
||||||
self.channel.notify(self.check)
|
self.channel.notify(self.check)
|
||||||
|
@ -150,25 +150,28 @@ class Webhook(HttpTransport):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def is_noop(self, check):
|
def is_noop(self, check):
|
||||||
if check.status == "down" and not self.channel.value_down:
|
if check.status == "down" and not self.channel.url_down:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if check.status == "up" and not self.channel.value_up:
|
if check.status == "up" and not self.channel.url_up:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def notify(self, check):
|
def notify(self, check):
|
||||||
url = self.channel.value_down
|
url = self.channel.url_down
|
||||||
if check.status == "up":
|
if check.status == "up":
|
||||||
url = self.channel.value_up
|
url = self.channel.url_up
|
||||||
|
|
||||||
assert url
|
assert url
|
||||||
|
|
||||||
url = self.prepare(url, check, urlencode=True)
|
url = self.prepare(url, check, urlencode=True)
|
||||||
if self.channel.post_data:
|
if self.channel.post_data:
|
||||||
payload = self.prepare(self.channel.post_data, check)
|
payload = self.prepare(self.channel.post_data, check)
|
||||||
return self.post(url, data=payload.encode("utf-8"))
|
headers = {}
|
||||||
|
if self.channel.headers:
|
||||||
|
headers = self.channel.headers
|
||||||
|
return self.post(url, data=payload.encode("utf-8"), headers=headers)
|
||||||
else:
|
else:
|
||||||
return self.get(url)
|
return self.get(url)
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from datetime import timedelta as td
|
from datetime import timedelta as td
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -57,17 +58,30 @@ class AddUrlForm(forms.Form):
|
|||||||
class AddWebhookForm(forms.Form):
|
class AddWebhookForm(forms.Form):
|
||||||
error_css_class = "has-error"
|
error_css_class = "has-error"
|
||||||
|
|
||||||
value_down = forms.URLField(max_length=1000, required=False,
|
url_down = forms.URLField(max_length=1000, required=False,
|
||||||
validators=[WebhookValidator()])
|
validators=[WebhookValidator()])
|
||||||
|
|
||||||
value_up = forms.URLField(max_length=1000, required=False,
|
url_up = forms.URLField(max_length=1000, required=False,
|
||||||
validators=[WebhookValidator()])
|
validators=[WebhookValidator()])
|
||||||
|
|
||||||
post_data = forms.CharField(max_length=1000, required=False)
|
post_data = forms.CharField(max_length=1000, required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.headers = {}
|
||||||
|
if all(k in kwargs for k in ("header_keys", "header_values")):
|
||||||
|
header_keys = kwargs.pop("header_keys")
|
||||||
|
header_values = kwargs.pop("header_values")
|
||||||
|
|
||||||
|
for i, (key, val) in enumerate(zip(header_keys, header_values)):
|
||||||
|
if key:
|
||||||
|
self.headers[key] = val
|
||||||
|
|
||||||
|
super(AddWebhookForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_value(self):
|
def get_value(self):
|
||||||
d = self.cleaned_data
|
val = dict(self.cleaned_data)
|
||||||
return "\n".join((d["value_down"], d["value_up"], d["post_data"]))
|
val["headers"] = self.headers
|
||||||
|
return json.dumps(val, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
phone_validator = RegexValidator(regex='^\+\d{5,15}$',
|
phone_validator = RegexValidator(regex='^\+\d{5,15}$',
|
||||||
|
@ -11,17 +11,17 @@ class AddWebhookTestCase(BaseTestCase):
|
|||||||
self.assertContains(r, "Runs a HTTP GET or HTTP POST")
|
self.assertContains(r, "Runs a HTTP GET or HTTP POST")
|
||||||
|
|
||||||
def test_it_adds_two_webhook_urls_and_redirects(self):
|
def test_it_adds_two_webhook_urls_and_redirects(self):
|
||||||
form = {"value_down": "http://foo.com", "value_up": "https://bar.com"}
|
form = {"url_down": "http://foo.com", "url_up": "https://bar.com"}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, form)
|
||||||
self.assertRedirects(r, "/integrations/")
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(c.value, "http://foo.com\nhttps://bar.com\n")
|
self.assertEqual(c.value, '{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}')
|
||||||
|
|
||||||
def test_it_adds_webhook_using_team_access(self):
|
def test_it_adds_webhook_using_team_access(self):
|
||||||
form = {"value_down": "http://foo.com", "value_up": "https://bar.com"}
|
form = {"url_down": "http://foo.com", "url_up": "https://bar.com"}
|
||||||
|
|
||||||
# Logging in as bob, not alice. Bob has team access so this
|
# Logging in as bob, not alice. Bob has team access so this
|
||||||
# should work.
|
# should work.
|
||||||
@ -30,7 +30,7 @@ class AddWebhookTestCase(BaseTestCase):
|
|||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(c.user, self.alice)
|
self.assertEqual(c.user, self.alice)
|
||||||
self.assertEqual(c.value, "http://foo.com\nhttps://bar.com\n")
|
self.assertEqual(c.value, '{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}')
|
||||||
|
|
||||||
def test_it_rejects_bad_urls(self):
|
def test_it_rejects_bad_urls(self):
|
||||||
urls = [
|
urls = [
|
||||||
@ -45,7 +45,7 @@ class AddWebhookTestCase(BaseTestCase):
|
|||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
for url in urls:
|
for url in urls:
|
||||||
form = {"value_down": url, "value_up": ""}
|
form = {"url_down": url, "url_up": ""}
|
||||||
|
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, form)
|
||||||
self.assertContains(r, "Enter a valid URL.", msg_prefix=url)
|
self.assertContains(r, "Enter a valid URL.", msg_prefix=url)
|
||||||
@ -53,20 +53,31 @@ class AddWebhookTestCase(BaseTestCase):
|
|||||||
self.assertEqual(Channel.objects.count(), 0)
|
self.assertEqual(Channel.objects.count(), 0)
|
||||||
|
|
||||||
def test_it_handles_empty_down_url(self):
|
def test_it_handles_empty_down_url(self):
|
||||||
form = {"value_down": "", "value_up": "http://foo.com"}
|
form = {"url_down": "", "url_up": "http://foo.com"}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
self.client.post(self.url, form)
|
self.client.post(self.url, form)
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(c.value, "\nhttp://foo.com\n")
|
self.assertEqual(c.value, '{"headers": {}, "post_data": "", "url_down": "", "url_up": "http://foo.com"}')
|
||||||
|
|
||||||
def test_it_adds_post_data(self):
|
def test_it_adds_post_data(self):
|
||||||
form = {"value_down": "http://foo.com", "post_data": "hello"}
|
form = {"url_down": "http://foo.com", "post_data": "hello"}
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, form)
|
||||||
self.assertRedirects(r, "/integrations/")
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
self.assertEqual(c.value, "http://foo.com\n\nhello")
|
self.assertEqual(c.value, '{"headers": {}, "post_data": "hello", "url_down": "http://foo.com", "url_up": ""}')
|
||||||
|
|
||||||
|
def test_it_adds_headers(self):
|
||||||
|
form = {"url_down": "http://foo.com", "header_key[]": ["test", "test2"], "header_value[]": ["123", "abc"]}
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.post(self.url, form)
|
||||||
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
|
c = Channel.objects.get()
|
||||||
|
self.assertEqual(c.value, '{"headers": {"test": "123", "test2": "abc"}, "post_data": "", "url_down": "http://foo.com", "url_up": ""}')
|
||||||
|
|
||||||
|
@ -437,7 +437,10 @@ def add_email(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def add_webhook(request):
|
def add_webhook(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = AddWebhookForm(request.POST)
|
header_keys = request.POST.getlist('header_key[]')
|
||||||
|
header_values = request.POST.getlist('header_value[]')
|
||||||
|
form = AddWebhookForm(request.POST or None,
|
||||||
|
header_keys=header_keys, header_values=header_values)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
channel = Channel(user=request.team.user, kind="webhook")
|
channel = Channel(user=request.team.user, kind="webhook")
|
||||||
channel.value = form.get_value()
|
channel.value = form.get_value()
|
||||||
|
29
static/js/webhook.js
Normal file
29
static/js/webhook.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
$(function() {
|
||||||
|
$(".webhook_header_btn:first").addClass("btn-info").text("+")
|
||||||
|
$(".webhook_header_btn:not(:first)").addClass("btn-danger").text("X")
|
||||||
|
|
||||||
|
$("#webhook_headers").on("click", ".webhook_header_btn.btn-danger", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).closest("div.row").remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#webhook_headers").on("click", ".webhook_header_btn.btn-info", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Add new header form
|
||||||
|
$("#webhook_headers").append(
|
||||||
|
'<div class="row">\
|
||||||
|
<div class="col-xs-6 col-sm-6" style="padding-right: 0px;">\
|
||||||
|
<input type="text" class="form-control" name="header_key[]" placeholder="Key">\
|
||||||
|
</div>\
|
||||||
|
<div class="col-xs-6 col-sm-6" style="padding-left: 0px;">\
|
||||||
|
<div class="input-group">\
|
||||||
|
<input type="text" class="form-control" name="header_value[]" placeholder="Value">\
|
||||||
|
<span class="input-group-btn">\
|
||||||
|
<button class="webhook_header_btn btn btn-danger" type="button" class="btn">X</button>\
|
||||||
|
</span>\
|
||||||
|
</div>\
|
||||||
|
</div>\
|
||||||
|
</div>');
|
||||||
|
});
|
||||||
|
});
|
@ -71,16 +71,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif ch.kind == "webhook" %}
|
{% elif ch.kind == "webhook" %}
|
||||||
<table>
|
<table>
|
||||||
{% if ch.value_down %}
|
{% if ch.url_down %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="preposition">down </td>
|
<td class="preposition">down </td>
|
||||||
<td>{{ ch.value_down }}</td>
|
<td>{{ ch.url_down }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ch.value_up %}
|
{% if ch.url_up %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="preposition">up </td>
|
<td class="preposition">up </td>
|
||||||
<td>{{ ch.value_up }}</td>
|
<td>{{ ch.url_up }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ch.post_data %}
|
{% if ch.post_data %}
|
||||||
@ -89,6 +89,12 @@
|
|||||||
<td>{{ ch.post_data }}</td>
|
<td>{{ ch.post_data }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if ch.headers %}
|
||||||
|
<tr>
|
||||||
|
<td class="preposition">headers </td>
|
||||||
|
<td>{{ ch.headers }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{% elif ch.kind == "pushbullet" %}
|
{% elif ch.kind == "pushbullet" %}
|
||||||
<span class="preposition">API key</span>
|
<span class="preposition">API key</span>
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
{% elif event.channel.kind == "po" %}
|
{% elif event.channel.kind == "po" %}
|
||||||
Sent a Pushover notification
|
Sent a Pushover notification
|
||||||
{% elif event.channel.kind == "webhook" %}
|
{% elif event.channel.kind == "webhook" %}
|
||||||
Called webhook {{ event.channel.value_down }}
|
Called webhook {{ event.channel.url_down }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Sent alert to {{ event.channel.kind|capfirst }}
|
Sent alert to {{ event.channel.kind|capfirst }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -57,34 +57,34 @@
|
|||||||
<form method="post" class="form-horizontal">
|
<form method="post" class="form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="kind" value="webhook" />
|
<input type="hidden" name="kind" value="webhook" />
|
||||||
<div class="form-group {{ form.value_down.css_classes }}">
|
<div class="form-group {{ form.url_down.css_classes }}">
|
||||||
<label class="col-sm-2 control-label">URL for "down" events</label>
|
<label class="col-sm-2 control-label">URL for "down" events</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="value_down"
|
name="url_down"
|
||||||
placeholder="http://..."
|
placeholder="http://..."
|
||||||
value="{{ form.value_down.value|default:"" }}">
|
value="{{ form.url_down.value|default:"" }}">
|
||||||
{% if form.value_down.errors %}
|
{% if form.url_down.errors %}
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
{{ form.value_down.errors|join:"" }}
|
{{ form.url_down.errors|join:"" }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group {{ form.value_up.css_classes }}">
|
<div class="form-group {{ form.url_up.css_classes }}">
|
||||||
<label class="col-sm-2 control-label">URL for "up" events</label>
|
<label class="col-sm-2 control-label">URL for "up" events</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="value_up"
|
name="url_up"
|
||||||
placeholder="http://..."
|
placeholder="http://..."
|
||||||
value="{{ form.value_up.value|default:"" }}">
|
value="{{ form.url_up.value|default:"" }}">
|
||||||
{% if form.value_up.errors %}
|
{% if form.url_up.errors %}
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
{{ form.value_up.errors|join:"" }}
|
{{ form.url_up.errors|join:"" }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -105,6 +105,42 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group {{ form.headers.css_classes }}">
|
||||||
|
<label class="col-sm-2 control-label">Headers</label>
|
||||||
|
<div id="webhook_headers" class="col-xs-12 col-sm-10">
|
||||||
|
{% if form.headers %}
|
||||||
|
{% for k,v in form.headers.items %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-6 col-sm-6" style="padding-right: 0px;">
|
||||||
|
<input type="text" class="form-control" name="header_key[]" placeholder="Key" value="{{ k|default:"" }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6 col-sm-6" style="padding-left: 0px;">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" name="header_value[]" placeholder="Value" value="{{ v|default:"" }}">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="webhook_header_btn btn" type="button" class="btn"></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-6 col-sm-6" style="padding-right: 0px;">
|
||||||
|
<input type="text" class="form-control" name="header_key[]" placeholder="Key">
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6 col-sm-6" style="padding-left: 0px;">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" name="header_value[]" placeholder="Value">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="webhook_header_btn btn btn-info" type="button" class="btn">+</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||||
@ -114,3 +150,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% compress js %}
|
||||||
|
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/webhook.js' %}"></script>
|
||||||
|
{% endcompress %}
|
||||||
|
{% endblock %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user