Add UI views for webhooks

This commit is contained in:
jeremystretch 2021-06-23 21:24:23 -04:00
parent 10cbbee947
commit 4e0b795a3c
11 changed files with 426 additions and 57 deletions

View File

@ -1,60 +1,6 @@
from django import forms
from django.contrib import admin
from django.contrib.contenttypes.models import ContentType
from utilities.forms import ContentTypeMultipleChoiceField, LaxURLField
from .models import JobResult, Webhook
from .utils import FeatureQuery
#
# Webhooks
#
class WebhookForm(forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks')
)
payload_url = LaxURLField(
label='URL'
)
class Meta:
model = Webhook
exclude = ()
@admin.register(Webhook)
class WebhookAdmin(admin.ModelAdmin):
list_display = [
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
'ssl_verification',
]
list_filter = [
'enabled', 'type_create', 'type_update', 'type_delete', 'content_types',
]
form = WebhookForm
fieldsets = (
(None, {
'fields': ('name', 'content_types', 'enabled')
}),
('Events', {
'fields': ('type_create', 'type_update', 'type_delete')
}),
('HTTP Request', {
'fields': (
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
),
'classes': ('monospace',)
}),
('SSL', {
'fields': ('ssl_verification', 'ca_file_path')
})
)
def models(self, obj):
return ', '.join([ct.name for ct in obj.content_types.all()])
from .models import JobResult
#

View File

@ -268,6 +268,129 @@ class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
)
#
# Webhooks
#
class WebhookForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks')
)
class Meta:
model = Webhook
fields = '__all__'
fieldsets = (
('Webhook', ('name', 'enabled')),
('Assigned Models', ('content_types',)),
('Events', ('type_create', 'type_update', 'type_delete')),
('HTTP Request', (
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
)),
('SSL', ('ssl_verification', 'ca_file_path')),
)
class WebhookCSVForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks'),
help_text="One or more assigned object types"
)
class Meta:
model = Webhook
fields = (
'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'payload_url',
'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'ssl_verification',
'ca_file_path'
)
class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Webhook.objects.all(),
widget=forms.MultipleHiddenInput
)
enabled = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
type_create = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
type_update = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
type_delete = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
http_method = forms.ChoiceField(
choices=WebhookHttpMethodChoices,
required=False
)
payload_url = forms.CharField(
required=False
)
ssl_verification = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
secret = forms.CharField(
required=False
)
ca_file_path = forms.CharField(
required=False
)
class Meta:
nullable_fields = ['secret', 'ca_file_path']
class WebhookFilterForm(BootstrapMixin, forms.Form):
field_groups = [
['content_types', 'http_method'],
['enabled', 'type_create', 'type_update', 'type_delete'],
]
content_types = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields')
)
http_method = forms.MultipleChoiceField(
choices=WebhookHttpMethodChoices,
required=False,
widget=StaticSelect2Multiple()
)
enabled = forms.NullBooleanField(
required=False,
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
type_create = forms.NullBooleanField(
required=False,
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
type_update = forms.NullBooleanField(
required=False,
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
type_delete = forms.NullBooleanField(
required=False,
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
#
# Custom field models
#

View File

@ -38,4 +38,14 @@ class Migration(migrations.Migration):
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='webhook',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='webhook',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@ -36,7 +36,8 @@ __all__ = (
# Webhooks
#
class Webhook(BigIDModel):
@extras_features('webhooks')
class Webhook(ChangeLoggedModel):
"""
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
@ -129,6 +130,9 @@ class Webhook(BigIDModel):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('extras:webhook', args=[self.pk])
def clean(self):
super().clean()

View File

@ -86,6 +86,27 @@ class ExportTemplateTable(BaseTable):
)
#
# Webhooks
#
class WebhookTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
class Meta(BaseTable.Meta):
model = Webhook
fields = (
'pk', 'name', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'payload_url',
'secret', 'ssl_validation', 'ca_file_path',
)
default_columns = (
'pk', 'name', 'enabled', 'type_create', 'type_update', 'type_delete', 'http_method', 'payload_url',
)
#
# Tags
#

View File

@ -120,6 +120,49 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
class WebhookTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Webhook
@classmethod
def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site)
webhooks = (
Webhook(name='Webhook 1', payload_url='http://example.com/?1', type_create=True, http_method='POST'),
Webhook(name='Webhook 2', payload_url='http://example.com/?2', type_create=True, http_method='POST'),
Webhook(name='Webhook 3', payload_url='http://example.com/?3', type_create=True, http_method='POST'),
)
for webhook in webhooks:
webhook.save()
webhook.content_types.add(site_ct)
cls.form_data = {
'name': 'Webhook X',
'content_types': [site_ct.pk],
'type_create': False,
'type_update': True,
'type_delete': True,
'payload_url': 'http://example.com/?x',
'http_method': 'GET',
'http_content_type': 'application/foo',
}
cls.csv_data = (
"name,content_types,type_create,payload_url,http_method,http_content_type",
"Webhook 4,dcim.site,True,http://example.com/?4,GET,application/json",
"Webhook 5,dcim.site,True,http://example.com/?5,GET,application/json",
"Webhook 6,dcim.site,True,http://example.com/?6,GET,application/json",
)
cls.bulk_edit_data = {
'enabled': False,
'type_create': False,
'type_update': True,
'type_delete': True,
'http_method': 'GET',
}
class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Tag

View File

@ -42,6 +42,18 @@ urlpatterns = [
path('export-templates/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog',
kwargs={'model': models.ExportTemplate}),
# Webhooks
path('webhooks/', views.WebhookListView.as_view(), name='webhook_list'),
path('webhooks/add/', views.WebhookEditView.as_view(), name='webhook_add'),
path('webhooks/import/', views.WebhookBulkImportView.as_view(), name='webhook_import'),
path('webhooks/edit/', views.WebhookBulkEditView.as_view(), name='webhook_bulk_edit'),
path('webhooks/delete/', views.WebhookBulkDeleteView.as_view(), name='webhook_bulk_delete'),
path('webhooks/<int:pk>/', views.WebhookView.as_view(), name='webhook'),
path('webhooks/<int:pk>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'),
path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'),
path('webhooks/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='webhook_changelog',
kwargs={'model': models.Webhook}),
# Tags
path('tags/', views.TagListView.as_view(), name='tag_list'),
path('tags/add/', views.TagEditView.as_view(), name='tag_add'),

View File

@ -149,6 +149,49 @@ class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
table = tables.ExportTemplateTable
#
# Webhooks
#
class WebhookListView(generic.ObjectListView):
queryset = Webhook.objects.all()
filterset = filtersets.WebhookFilterSet
filterset_form = forms.WebhookFilterForm
table = tables.WebhookTable
class WebhookView(generic.ObjectView):
queryset = Webhook.objects.all()
class WebhookEditView(generic.ObjectEditView):
queryset = Webhook.objects.all()
model_form = forms.WebhookForm
class WebhookDeleteView(generic.ObjectDeleteView):
queryset = Webhook.objects.all()
class WebhookBulkImportView(generic.BulkImportView):
queryset = Webhook.objects.all()
model_form = forms.WebhookCSVForm
table = tables.WebhookTable
class WebhookBulkEditView(generic.BulkEditView):
queryset = Webhook.objects.all()
filterset = filtersets.WebhookFilterSet
table = tables.WebhookTable
form = forms.WebhookBulkEditForm
class WebhookBulkDeleteView(generic.BulkDeleteView):
queryset = Webhook.objects.all()
filterset = filtersets.WebhookFilterSet
table = tables.WebhookTable
#
# Tags
#

View File

@ -3,7 +3,7 @@
{% load plugins %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'extras:customfield_list' %}">Cusotm Fields</a></li>
<li class="breadcrumb-item"><a href="{% url 'extras:customfield_list' %}">Custom Fields</a></li>
<li class="breadcrumb-item">{{ object }}</li>
{% endblock %}

View File

@ -0,0 +1,165 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'extras:webhook_list' %}">Webhooks</a></li>
<li class="breadcrumb-item">{{ object }}</li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Webhook
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Enabled</th>
<td>
{% if object.enabled %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">
Events
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Create</th>
<td>
{% if object.type_create %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Update</th>
<td>
{% if object.type_create %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Delete</th>
<td>
{% if object.type_create %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">
HTTP Request
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">HTTP Method</th>
<td>{{ object.get_http_method_display }}</td>
</tr>
<tr>
<th scope="row">Payload URL</th>
<td><code>{{ object.payload_url }}</code></td>
</tr>
<tr>
<th scope="row">HTTP Content Type</th>
<td>{{ object.http_content_type }}</td>
</tr>
<tr>
<th scope="row">Secret</th>
<td>{{ object.secret|placeholder }}</td>
</tr>
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">
SSL
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">SSL Verification</th>
<td>
{% if object.ssl_verification %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">CA File Path</th>
<td>
{% if object.ca_file_path %}
<code>{{ object.ca_file_path }}</code>
{% else %}
&mdash
{% endif %}
</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Assigned Models
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
{% for ct in object.content_types.all %}
<tr>
<td>{{ ct }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">
Additional Headers
</h5>
<div class="card-body">
<pre>{{ object.additional_headers }}</pre>
</div>
</div>
<div class="card">
<h5 class="card-header">
Body Template
</h5>
<div class="card-body">
<pre>{{ object.body_template }}</pre>
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
{% endblock %}

View File

@ -287,6 +287,8 @@ OTHER_MENU = Menu(
add_url=None, import_url=None),
MenuItem(label="Journal Entries",
url="extras:journalentry_list", add_url=None, import_url=None),
MenuItem(label="Webhooks", url="extras:webhook_list",
add_url="extras:webhook_add", import_url="extras:webhook_import"),
),
),
MenuGroup(