jeremystretch 4 лет назад
Родитель
Сommit
4e0b795a3c

+ 1 - 55
netbox/extras/admin.py

@@ -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
 
 
 #

+ 123 - 0
netbox/extras/forms.py

@@ -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
 #

+ 10 - 0
netbox/extras/migrations/0061_extras_change_logging.py

@@ -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),
+        ),
     ]

+ 5 - 1
netbox/extras/models/models.py

@@ -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()
 

+ 21 - 0
netbox/extras/tables.py

@@ -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
 #

+ 43 - 0
netbox/extras/tests/test_views.py

@@ -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
 

+ 12 - 0
netbox/extras/urls.py

@@ -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'),

+ 43 - 0
netbox/extras/views.py

@@ -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
 #

+ 1 - 1
netbox/templates/extras/customfield.html

@@ -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 %}
 

+ 165 - 0
netbox/templates/extras/webhook.html

@@ -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 %}

+ 2 - 0
netbox/utilities/templatetags/nav.py

@@ -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(