diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 1555206ed..3cf1794f2 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -2,8 +2,8 @@ from django import forms from django.contrib import admin from django.contrib.contenttypes.models import ContentType -from utilities.forms import ContentTypeChoiceField, ContentTypeMultipleChoiceField, LaxURLField -from .models import CustomLink, ExportTemplate, JobResult, Webhook +from utilities.forms import ContentTypeMultipleChoiceField, LaxURLField +from .models import JobResult, Webhook from .utils import FeatureQuery @@ -57,41 +57,6 @@ class WebhookAdmin(admin.ModelAdmin): return ', '.join([ct.name for ct in obj.content_types.all()]) -# -# Export templates -# - -class ExportTemplateForm(forms.ModelForm): - content_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_links') - ) - - class Meta: - model = ExportTemplate - exclude = [] - - -@admin.register(ExportTemplate) -class ExportTemplateAdmin(admin.ModelAdmin): - fieldsets = ( - ('Export Template', { - 'fields': ('content_type', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment') - }), - ('Content', { - 'fields': ('template_code',), - 'classes': ('monospace',) - }) - ) - list_display = [ - 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', - ] - list_filter = [ - 'content_type', - ] - form = ExportTemplateForm - - # # Reports # diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index afc32f8b6..7a8a37ff9 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -123,7 +123,7 @@ class CustomLinkCSVForm(CSVModelForm): content_type = CSVContentTypeField( queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('custom_links'), - help_text="One or more assigned object types" + help_text="Assigned object type" ) class Meta: @@ -180,6 +180,94 @@ class CustomLinkFilterForm(BootstrapMixin, forms.Form): ) +# +# Export templates +# + +class ExportTemplateForm(BootstrapMixin, forms.ModelForm): + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_links') + ) + + class Meta: + model = ExportTemplate + fields = '__all__' + fieldsets = ( + ('Custom Link', ('name', 'content_type', 'description')), + ('Template', ('template_code',)), + ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), + ) + + +class ExportTemplateCSVForm(CSVModelForm): + content_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('export_templates'), + help_text="Assigned object type" + ) + + class Meta: + model = ExportTemplate + fields = ( + 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', + ) + + +class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ExportTemplate.objects.all(), + widget=forms.MultipleHiddenInput + ) + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields'), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + mime_type = forms.CharField( + max_length=50, + required=False + ) + file_extension = forms.CharField( + max_length=15, + required=False + ) + as_attachment = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + + class Meta: + nullable_fields = ['description', 'mime_type', 'file_extension'] + + +class ExportTemplateFilterForm(BootstrapMixin, forms.Form): + field_groups = [ + ['content_type', 'mime_type'], + ['file_extension', 'as_attachment'], + ] + content_type = ContentTypeChoiceField( + queryset=ContentType.objects.all(), + limit_choices_to=FeatureQuery('custom_fields') + ) + mime_type = forms.CharField( + required=False + ) + file_extension = forms.CharField( + required=False + ) + as_attachment = forms.NullBooleanField( + required=False, + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + # # Custom field models # diff --git a/netbox/extras/migrations/0061_extras_change_logging.py b/netbox/extras/migrations/0061_extras_change_logging.py index 405855e54..cd8531f89 100644 --- a/netbox/extras/migrations/0061_extras_change_logging.py +++ b/netbox/extras/migrations/0061_extras_change_logging.py @@ -28,4 +28,14 @@ class Migration(migrations.Migration): name='last_updated', field=models.DateTimeField(auto_now=True, null=True), ), + migrations.AddField( + model_name='exporttemplate', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='exporttemplate', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index b8e1acc81..6d38ed9c3 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -171,6 +171,7 @@ class Webhook(BigIDModel): # Custom links # +@extras_features('webhooks') class CustomLink(ChangeLoggedModel): """ A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template @@ -229,7 +230,8 @@ class CustomLink(ChangeLoggedModel): # Export templates # -class ExportTemplate(BigIDModel): +@extras_features('webhooks') +class ExportTemplate(ChangeLoggedModel): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, @@ -272,6 +274,9 @@ class ExportTemplate(BigIDModel): def __str__(self): return f"{self.content_type}: {self.name}" + def get_absolute_url(self): + return reverse('extras:exporttemplate', args=[self.pk]) + def clean(self): super().clean() diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 94ee6db64..66e623360 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -55,6 +55,7 @@ class CustomLinkTable(BaseTable): name = tables.Column( linkify=True ) + new_window = BooleanColumn() class Meta(BaseTable.Meta): model = CustomLink @@ -64,6 +65,27 @@ class CustomLinkTable(BaseTable): default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window') +# +# Export templates +# + +class ExportTemplateTable(BaseTable): + pk = ToggleColumn() + name = tables.Column( + linkify=True + ) + as_attachment = BooleanColumn() + + class Meta(BaseTable.Meta): + model = ExportTemplate + fields = ( + 'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', + ) + default_columns = ( + 'pk', 'name', 'content_type', 'description', 'mime_type', 'file_extension', 'as_attachment', + ) + + # # Tags # diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 75c1bbbaa..7a00f5f73 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -86,6 +86,40 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): } +class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): + model = ExportTemplate + + @classmethod + def setUpTestData(cls): + + site_ct = ContentType.objects.get_for_model(Site) + TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}""" + ExportTemplate.objects.bulk_create(( + ExportTemplate(name='Export Template 1', content_type=site_ct, template_code=TEMPLATE_CODE), + ExportTemplate(name='Export Template 2', content_type=site_ct, template_code=TEMPLATE_CODE), + ExportTemplate(name='Export Template 3', content_type=site_ct, template_code=TEMPLATE_CODE), + )) + + cls.form_data = { + 'name': 'Export Template X', + 'content_type': site_ct.pk, + 'template_code': TEMPLATE_CODE, + } + + cls.csv_data = ( + "name,content_type,template_code", + f"Export Template 4,dcim.site,{TEMPLATE_CODE}", + f"Export Template 5,dcim.site,{TEMPLATE_CODE}", + f"Export Template 6,dcim.site,{TEMPLATE_CODE}", + ) + + cls.bulk_edit_data = { + 'mime_type': 'text/html', + 'file_extension': 'html', + 'as_attachment': True, + } + + class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase): model = Tag diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 64e6814eb..6404f2677 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -30,6 +30,18 @@ urlpatterns = [ path('custom-links//changelog/', views.ObjectChangeLogView.as_view(), name='customlink_changelog', kwargs={'model': models.CustomLink}), + # Export templates + path('export-templates/', views.ExportTemplateListView.as_view(), name='exporttemplate_list'), + path('export-templates/add/', views.ExportTemplateEditView.as_view(), name='exporttemplate_add'), + path('export-templates/import/', views.ExportTemplateBulkImportView.as_view(), name='exporttemplate_import'), + path('export-templates/edit/', views.ExportTemplateBulkEditView.as_view(), name='exporttemplate_bulk_edit'), + path('export-templates/delete/', views.ExportTemplateBulkDeleteView.as_view(), name='exporttemplate_bulk_delete'), + path('export-templates//', views.ExportTemplateView.as_view(), name='exporttemplate'), + path('export-templates//edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'), + path('export-templates//delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'), + path('export-templates//changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog', + kwargs={'model': models.ExportTemplate}), + # 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 0259173cd..28089bd39 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -106,6 +106,49 @@ class CustomLinkBulkDeleteView(generic.BulkDeleteView): table = tables.CustomLinkTable +# +# Export templates +# + +class ExportTemplateListView(generic.ObjectListView): + queryset = ExportTemplate.objects.all() + filterset = filtersets.ExportTemplateFilterSet + filterset_form = forms.ExportTemplateFilterForm + table = tables.ExportTemplateTable + + +class ExportTemplateView(generic.ObjectView): + queryset = ExportTemplate.objects.all() + + +class ExportTemplateEditView(generic.ObjectEditView): + queryset = ExportTemplate.objects.all() + model_form = forms.ExportTemplateForm + + +class ExportTemplateDeleteView(generic.ObjectDeleteView): + queryset = ExportTemplate.objects.all() + + +class ExportTemplateBulkImportView(generic.BulkImportView): + queryset = ExportTemplate.objects.all() + model_form = forms.ExportTemplateCSVForm + table = tables.ExportTemplateTable + + +class ExportTemplateBulkEditView(generic.BulkEditView): + queryset = ExportTemplate.objects.all() + filterset = filtersets.ExportTemplateFilterSet + table = tables.ExportTemplateTable + form = forms.ExportTemplateBulkEditForm + + +class ExportTemplateBulkDeleteView(generic.BulkDeleteView): + queryset = ExportTemplate.objects.all() + filterset = filtersets.ExportTemplateFilterSet + table = tables.ExportTemplateTable + + # # Tags # diff --git a/netbox/templates/extras/customlink.html b/netbox/templates/extras/customlink.html index 488d76f86..f337fd6e4 100644 --- a/netbox/templates/extras/customlink.html +++ b/netbox/templates/extras/customlink.html @@ -3,7 +3,7 @@ {% load plugins %} {% block breadcrumbs %} - + {% endblock %} diff --git a/netbox/templates/extras/exporttemplate.html b/netbox/templates/extras/exporttemplate.html new file mode 100644 index 000000000..1da716d71 --- /dev/null +++ b/netbox/templates/extras/exporttemplate.html @@ -0,0 +1,66 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load plugins %} + +{% block breadcrumbs %} + + +{% endblock %} + +{% block content %} +
+
+
+
+ Export Template +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Content Type{{ object.content_type }}
Name{{ object.name }}
Description{{ object.description|placeholder }}
MIME Type{{ object.mime_type|placeholder }}
File Extension{{ object.file_extension|placeholder }}
Attachment + {% if object.as_attachment %} + + {% else %} + + {% endif %} +
+
+
+ {% plugin_left_page object %} +
+
+
+
+ Template +
+
+
{{ object.template_code }}
+
+
+ {% plugin_right_page object %} +
+
+{% endblock %} diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index e7ebd26a0..3608f5a7c 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -88,7 +88,7 @@ def export_button(context, content_type=None): user = context['request'].user export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_type=content_type) if user.is_staff and user.has_perm('extras.add_exporttemplate'): - add_exporttemplate_link = f"{reverse('admin:extras_exporttemplate_add')}?content_type={content_type.pk}" + add_exporttemplate_link = f"{reverse('extras:exporttemplate_add')}?content_type={content_type.pk}" else: export_templates = [] diff --git a/netbox/utilities/templatetags/nav.py b/netbox/utilities/templatetags/nav.py index 3373320c6..884a5971b 100644 --- a/netbox/utilities/templatetags/nav.py +++ b/netbox/utilities/templatetags/nav.py @@ -296,6 +296,8 @@ OTHER_MENU = Menu( add_url="extras:customfield_add", import_url="extras:customfield_import"), MenuItem(label="Custom Links", url="extras:customlink_list", add_url="extras:customlink_add", import_url="extras:customlink_import"), + MenuItem(label="Export Templates", url="extras:exporttemplate_list", + add_url="extras:exporttemplate_add", import_url="extras:exporttemplate_import"), ), ), MenuGroup(