From 38d46afdbe84fe1c216c50ca78e9447e02dd0f84 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 30 Oct 2023 14:57:51 -0700 Subject: [PATCH] 14132 add models and forms --- netbox/extras/filtersets.py | 26 +++++++ netbox/extras/forms/bulk_edit.py | 42 +++++++++++ netbox/extras/forms/bulk_import.py | 17 +++++ netbox/extras/forms/filtersets.py | 59 +++++++++++++++ netbox/extras/forms/model_forms.py | 29 +++++++ netbox/extras/migrations/0099_eventrule.py | 50 ++++++++++++ netbox/extras/models/models.py | 88 ++++++++++++++++++++++ netbox/extras/tables/tables.py | 43 +++++++++++ netbox/extras/urls.py | 8 ++ netbox/extras/views.py | 45 +++++++++++ 10 files changed, 407 insertions(+) create mode 100644 netbox/extras/migrations/0099_eventrule.py diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index fec067263..8492c2fb0 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -23,6 +23,7 @@ __all__ = ( 'CustomFieldChoiceSetFilterSet', 'CustomFieldFilterSet', 'CustomLinkFilterSet', + 'EventRuleFilterSet', 'ExportTemplateFilterSet', 'ImageAttachmentFilterSet', 'JournalEntryFilterSet', @@ -63,6 +64,31 @@ class WebhookFilterSet(NetBoxModelFilterSet): ) +class EventRuleFilterSet(NetBoxModelFilterSet): + q = django_filters.CharFilter( + method='search', + label=_('Search'), + ) + content_type_id = MultiValueNumberFilter( + field_name='content_types__id' + ) + content_types = ContentTypeFilter() + + class Meta: + model = EventRule + fields = [ + 'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', + 'enabled', + ] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) + ) + + class CustomFieldFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 821ce7eb2..ae9c654a4 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -14,6 +14,7 @@ __all__ = ( 'CustomFieldBulkEditForm', 'CustomFieldChoiceSetBulkEditForm', 'CustomLinkBulkEditForm', + 'EventRuleBulkEditForm', 'ExportTemplateBulkEditForm', 'JournalEntryBulkEditForm', 'SavedFilterBulkEditForm', @@ -229,6 +230,47 @@ class WebhookBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('secret', 'conditions', 'ca_file_path') +class EventRuleBulkEditForm(NetBoxModelBulkEditForm): + model = EventRule + + pk = forms.ModelMultipleChoiceField( + queryset=EventRule.objects.all(), + widget=forms.MultipleHiddenInput + ) + enabled = forms.NullBooleanField( + label=_('Enabled'), + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_create = forms.NullBooleanField( + label=_('On create'), + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_update = forms.NullBooleanField( + label=_('On update'), + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_delete = forms.NullBooleanField( + label=_('On delete'), + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_job_start = forms.NullBooleanField( + label=_('On job start'), + required=False, + widget=BulkEditNullBooleanSelect() + ) + type_job_end = forms.NullBooleanField( + label=_('On job end'), + required=False, + widget=BulkEditNullBooleanSelect() + ) + + nullable_fields = ('conditions',) + + class TagBulkEditForm(BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Tag.objects.all(), diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 79023a74d..22d0364fe 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -18,6 +18,7 @@ __all__ = ( 'CustomFieldChoiceSetImportForm', 'CustomFieldImportForm', 'CustomLinkImportForm', + 'EventRuleImportForm', 'ExportTemplateImportForm', 'JournalEntryImportForm', 'SavedFilterImportForm', @@ -157,6 +158,22 @@ class WebhookImportForm(NetBoxModelImportForm): ) +class EventRuleImportForm(NetBoxModelImportForm): + content_types = CSVMultipleContentTypeField( + label=_('Content types'), + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('eventrules'), + help_text=_("One or more assigned object types") + ) + + class Meta: + model = EventRule + fields = ( + 'name', 'enabled', 'content_types', 'type_create', 'type_update', 'type_delete', 'type_job_start', + 'type_job_end', 'tags' + ) + + class TagImportForm(CSVModelForm): slug = SlugField() diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 7db84d175..c75b84cb8 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -25,6 +25,7 @@ __all__ = ( 'CustomFieldChoiceSetFilterForm', 'CustomFieldFilterForm', 'CustomLinkFilterForm', + 'EventRuleFilterForm', 'ExportTemplateFilterForm', 'ImageAttachmentFilterForm', 'JournalEntryFilterForm', @@ -282,6 +283,64 @@ class WebhookFilterForm(NetBoxModelFilterSetForm): ) +class EventRuleFilterForm(NetBoxModelFilterSetForm): + model = EventRule + tag = TagFilterField(model) + + fieldsets = ( + (None, ('q', 'filter_id', 'tag')), + (_('Attributes'), ('content_type_id', 'enabled')), + (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), + ) + content_type_id = ContentTypeMultipleChoiceField( + queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()), + required=False, + label=_('Object type') + ) + enabled = forms.NullBooleanField( + label=_('Enabled'), + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + type_create = forms.NullBooleanField( + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ), + label=_('Object creations') + ) + type_update = forms.NullBooleanField( + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ), + label=_('Object updates') + ) + type_delete = forms.NullBooleanField( + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ), + label=_('Object deletions') + ) + type_job_start = forms.NullBooleanField( + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ), + label=_('Job starts') + ) + type_job_end = forms.NullBooleanField( + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ), + label=_('Job terminations') + ) + + class TagFilterForm(SavedFiltersMixin, FilterForm): model = Tag content_type_id = ContentTypeMultipleChoiceField( diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index fd2ce8f2d..ab8424705 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -32,6 +32,7 @@ __all__ = ( 'CustomFieldChoiceSetForm', 'CustomFieldForm', 'CustomLinkForm', + 'EventRuleForm', 'ExportTemplateForm', 'ImageAttachmentForm', 'JournalEntryForm', @@ -256,6 +257,34 @@ class WebhookForm(NetBoxModelForm): } +class EventRuleForm(NetBoxModelForm): + content_types = ContentTypeMultipleChoiceField( + label=_('Content types'), + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('eventrules') + ) + + fieldsets = ( + (_('EventRule'), ('name', 'content_types', 'enabled', 'tags')), + (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), + (_('Conditions'), ('conditions',)), + ) + + class Meta: + model = Webhook + fields = '__all__' + labels = { + 'type_create': _('Creations'), + 'type_update': _('Updates'), + 'type_delete': _('Deletions'), + 'type_job_start': _('Job executions'), + 'type_job_end': _('Job terminations'), + } + widgets = { + 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}), + } + + class TagForm(BootstrapMixin, forms.ModelForm): slug = SlugField() object_types = ContentTypeMultipleChoiceField( diff --git a/netbox/extras/migrations/0099_eventrule.py b/netbox/extras/migrations/0099_eventrule.py new file mode 100644 index 000000000..8be994752 --- /dev/null +++ b/netbox/extras/migrations/0099_eventrule.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.5 on 2023-10-30 21:57 + +from django.db import migrations, models +import extras.utils +import taggit.managers +import utilities.json + + +class Migration(migrations.Migration): + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('extras', '0098_webhook_custom_field_data_webhook_tags'), + ] + + operations = [ + migrations.CreateModel( + name='EventRule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ( + 'custom_field_data', + models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), + ), + ('name', models.CharField(max_length=150, unique=True)), + ('type_create', models.BooleanField(default=False)), + ('type_update', models.BooleanField(default=False)), + ('type_delete', models.BooleanField(default=False)), + ('type_job_start', models.BooleanField(default=False)), + ('type_job_end', models.BooleanField(default=False)), + ('enabled', models.BooleanField(default=True)), + ('conditions', models.JSONField(blank=True, null=True)), + ( + 'content_types', + models.ManyToManyField( + limit_choices_to=extras.utils.FeatureQuery('eventrules'), + related_name='eventrules', + to='contenttypes.contenttype', + ), + ), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'verbose_name': 'eventrule', + 'verbose_name_plural': 'eventrules', + 'ordering': ('name',), + }, + ), + ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 90e8027b4..be37e7d02 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -30,6 +30,7 @@ __all__ = ( 'Bookmark', 'ConfigRevision', 'CustomLink', + 'EventRule', 'ExportTemplate', 'ImageAttachment', 'JournalEntry', @@ -38,6 +39,93 @@ __all__ = ( ) +class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, 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. + Each Webhook can be limited to firing only on certain actions or certain object types. + """ + content_types = models.ManyToManyField( + to=ContentType, + related_name='eventrules', + verbose_name=_('object types'), + limit_choices_to=FeatureQuery('eventrules'), + help_text=_("The object(s) to which this Event applies.") + ) + name = models.CharField( + verbose_name=_('name'), + max_length=150, + unique=True + ) + type_create = models.BooleanField( + verbose_name=_('on create'), + default=False, + help_text=_("Triggers when a matching object is created.") + ) + type_update = models.BooleanField( + verbose_name=_('on update'), + default=False, + help_text=_("Triggers when a matching object is updated.") + ) + type_delete = models.BooleanField( + verbose_name=_('on delete'), + default=False, + help_text=_("Triggers when a matching object is deleted.") + ) + type_job_start = models.BooleanField( + verbose_name=_('on job start'), + default=False, + help_text=_("Triggers when a job for a matching object is started.") + ) + type_job_end = models.BooleanField( + verbose_name=_('on job end'), + default=False, + help_text=_("Triggers when a job for a matching object terminates.") + ) + enabled = models.BooleanField( + verbose_name=_('enabled'), + default=True + ) + conditions = models.JSONField( + verbose_name=_('conditions'), + blank=True, + null=True, + help_text=_("A set of conditions which determine whether the event will be generated.") + ) + + class Meta: + ordering = ('name',) + verbose_name = _('eventrule') + verbose_name_plural = _('eventrules') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('extras:eventrule', args=[self.pk]) + + @property + def docs_url(self): + return f'{settings.STATIC_URL}docs/models/extras/eventrule/' + + def clean(self): + super().clean() + + # At least one action type must be selected + if not any([ + self.type_create, self.type_update, self.type_delete, self.type_job_start, self.type_job_end + ]): + raise ValidationError( + _("At least one event type must be selected: create, update, delete, job_start, and/or job_end.") + ) + + if self.conditions: + try: + ConditionSet(self.conditions) + except ValueError as e: + raise ValidationError({'conditions': e}) + + class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): """ A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 9e14a2d27..c9c663722 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -16,6 +16,7 @@ __all__ = ( 'CustomFieldChoiceSetTable', 'CustomFieldTable', 'CustomLinkTable', + 'EventRuleTable', 'ExportTemplateTable', 'ImageAttachmentTable', 'JournalEntryTable', @@ -314,6 +315,48 @@ class WebhookTable(NetBoxTable): ) +class EventRuleTable(NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + linkify=True + ) + content_types = columns.ContentTypesColumn( + verbose_name=_('Content Types'), + ) + enabled = columns.BooleanColumn( + verbose_name=_('Enabled'), + ) + type_create = columns.BooleanColumn( + verbose_name=_('Create') + ) + type_update = columns.BooleanColumn( + verbose_name=_('Update') + ) + type_delete = columns.BooleanColumn( + verbose_name=_('Delete') + ) + type_job_start = columns.BooleanColumn( + verbose_name=_('Job Start') + ) + type_job_end = columns.BooleanColumn( + verbose_name=_('Job End') + ) + tags = columns.TagColumn( + url_name='extras:webhook_list' + ) + + class Meta(NetBoxTable.Meta): + model = EventRule + fields = ( + 'pk', 'id', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', + 'type_job_start', 'type_job_end', 'tags', 'created', 'last_updated', + ) + default_columns = ( + 'pk', 'name', 'content_types', 'enabled', 'type_create', 'type_update', 'type_delete', 'type_job_start', + 'type_job_end', + ) + + class TagTable(NetBoxTable): name = tables.Column( verbose_name=_('Name'), diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index fd95186e4..5cf002950 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -61,6 +61,14 @@ urlpatterns = [ path('webhooks/delete/', views.WebhookBulkDeleteView.as_view(), name='webhook_bulk_delete'), path('webhooks//', include(get_model_urls('extras', 'webhook'))), + # Event rules + path('event-rules/', views.EventRuleListView.as_view(), name='eventrule_list'), + path('event-rules/add/', views.EventRuleEditView.as_view(), name='eventrule_add'), + path('event-rules/import/', views.EventRuleBulkImportView.as_view(), name='eventrule_import'), + path('event-rules/edit/', views.EventRuleBulkEditView.as_view(), name='eventrule_bulk_edit'), + path('event-rules/delete/', views.EventRuleBulkDeleteView.as_view(), name='eventrule_bulk_delete'), + path('event-rules//', include(get_model_urls('extras', 'eventrule'))), + # Tags path('tags/', views.TagListView.as_view(), name='tag_list'), path('tags/add/', views.TagEditView.as_view(), name='tag_add'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 0e8e3b0ea..0af437ca9 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -396,6 +396,51 @@ class WebhookBulkDeleteView(generic.BulkDeleteView): table = tables.WebhookTable +# +# Event Rules +# + +class EventRuleListView(generic.ObjectListView): + queryset = EventRule.objects.all() + filterset = filtersets.EventRuleFilterSet + filterset_form = forms.EventRuleFilterForm + table = tables.EventRuleTable + + +@register_model_view(EventRule) +class EventRuleView(generic.ObjectView): + queryset = EventRule.objects.all() + + +@register_model_view(EventRule, 'edit') +class EventRuleEditView(generic.ObjectEditView): + queryset = EventRule.objects.all() + form = forms.EventRuleForm + + +@register_model_view(EventRule, 'delete') +class EventRuleDeleteView(generic.ObjectDeleteView): + queryset = EventRule.objects.all() + + +class EventRuleBulkImportView(generic.BulkImportView): + queryset = EventRule.objects.all() + model_form = forms.EventRuleImportForm + + +class EventRuleBulkEditView(generic.BulkEditView): + queryset = EventRule.objects.all() + filterset = filtersets.EventRuleFilterSet + table = tables.EventRuleTable + form = forms.EventRuleBulkEditForm + + +class EventRuleBulkDeleteView(generic.BulkDeleteView): + queryset = EventRule.objects.all() + filterset = filtersets.EventRuleFilterSet + table = tables.EventRuleTable + + # # Tags #