1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

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 import admin
from django.contrib.contenttypes.models import ContentType
from utilities.forms import ContentTypeMultipleChoiceField, LaxURLField from .models import JobResult
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()])
# #

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 # Custom field models
# #

View File

@ -38,4 +38,14 @@ class Migration(migrations.Migration):
name='last_updated', name='last_updated',
field=models.DateTimeField(auto_now=True, null=True), 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 # 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 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. 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): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse('extras:webhook', args=[self.pk])
def clean(self): def clean(self):
super().clean() 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 # 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): class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Tag model = Tag

View File

@ -42,6 +42,18 @@ urlpatterns = [
path('export-templates/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog', path('export-templates/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog',
kwargs={'model': models.ExportTemplate}), 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 # Tags
path('tags/', views.TagListView.as_view(), name='tag_list'), path('tags/', views.TagListView.as_view(), name='tag_list'),
path('tags/add/', views.TagEditView.as_view(), name='tag_add'), path('tags/add/', views.TagEditView.as_view(), name='tag_add'),

View File

@ -149,6 +149,49 @@ class ExportTemplateBulkDeleteView(generic.BulkDeleteView):
table = tables.ExportTemplateTable 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 # Tags
# #

View File

@ -3,7 +3,7 @@
{% load plugins %} {% load plugins %}
{% block breadcrumbs %} {% 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> <li class="breadcrumb-item">{{ object }}</li>
{% endblock %} {% 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), add_url=None, import_url=None),
MenuItem(label="Journal Entries", MenuItem(label="Journal Entries",
url="extras:journalentry_list", add_url=None, import_url=None), 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( MenuGroup(