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

Add UI views for export templates

This commit is contained in:
jeremystretch
2021-06-23 20:39:35 -04:00
parent 276ded0119
commit 10cbbee947
12 changed files with 288 additions and 41 deletions

View File

@ -2,8 +2,8 @@ from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from utilities.forms import ContentTypeChoiceField, ContentTypeMultipleChoiceField, LaxURLField from utilities.forms import ContentTypeMultipleChoiceField, LaxURLField
from .models import CustomLink, ExportTemplate, JobResult, Webhook from .models import JobResult, Webhook
from .utils import FeatureQuery from .utils import FeatureQuery
@ -57,41 +57,6 @@ class WebhookAdmin(admin.ModelAdmin):
return ', '.join([ct.name for ct in obj.content_types.all()]) 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 # Reports
# #

View File

@ -123,7 +123,7 @@ class CustomLinkCSVForm(CSVModelForm):
content_type = CSVContentTypeField( content_type = CSVContentTypeField(
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links'), limit_choices_to=FeatureQuery('custom_links'),
help_text="One or more assigned object types" help_text="Assigned object type"
) )
class Meta: 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 # Custom field models
# #

View File

@ -28,4 +28,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='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),
),
] ]

View File

@ -171,6 +171,7 @@ class Webhook(BigIDModel):
# Custom links # Custom links
# #
@extras_features('webhooks')
class CustomLink(ChangeLoggedModel): class CustomLink(ChangeLoggedModel):
""" """
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template 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 # Export templates
# #
class ExportTemplate(BigIDModel): @extras_features('webhooks')
class ExportTemplate(ChangeLoggedModel):
content_type = models.ForeignKey( content_type = models.ForeignKey(
to=ContentType, to=ContentType,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -272,6 +274,9 @@ class ExportTemplate(BigIDModel):
def __str__(self): def __str__(self):
return f"{self.content_type}: {self.name}" return f"{self.content_type}: {self.name}"
def get_absolute_url(self):
return reverse('extras:exporttemplate', args=[self.pk])
def clean(self): def clean(self):
super().clean() super().clean()

View File

@ -55,6 +55,7 @@ class CustomLinkTable(BaseTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
new_window = BooleanColumn()
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CustomLink model = CustomLink
@ -64,6 +65,27 @@ class CustomLinkTable(BaseTable):
default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window') 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 # Tags
# #

View File

@ -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): class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Tag model = Tag

View File

@ -30,6 +30,18 @@ urlpatterns = [
path('custom-links/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customlink_changelog', path('custom-links/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customlink_changelog',
kwargs={'model': models.CustomLink}), 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/<int:pk>/', views.ExportTemplateView.as_view(), name='exporttemplate'),
path('export-templates/<int:pk>/edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'),
path('export-templates/<int:pk>/delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'),
path('export-templates/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog',
kwargs={'model': models.ExportTemplate}),
# 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

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

View File

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

View File

@ -0,0 +1,66 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'extras:exporttemplate_list' %}">Export Templates</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">
Export Template
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Content Type</th>
<td>{{ object.content_type }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">MIME Type</th>
<td>{{ object.mime_type|placeholder }}</td>
</tr>
<tr>
<th scope="row">File Extension</th>
<td>{{ object.file_extension|placeholder }}</td>
</tr>
<tr>
<th scope="row">Attachment</th>
<td>
{% if object.as_attachment %}
<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>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Template
</h5>
<div class="card-body">
<pre>{{ object.template_code }}</pre>
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
{% endblock %}

View File

@ -88,7 +88,7 @@ def export_button(context, content_type=None):
user = context['request'].user user = context['request'].user
export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_type=content_type) export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_type=content_type)
if user.is_staff and user.has_perm('extras.add_exporttemplate'): 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: else:
export_templates = [] export_templates = []

View File

@ -296,6 +296,8 @@ OTHER_MENU = Menu(
add_url="extras:customfield_add", import_url="extras:customfield_import"), add_url="extras:customfield_add", import_url="extras:customfield_import"),
MenuItem(label="Custom Links", url="extras:customlink_list", MenuItem(label="Custom Links", url="extras:customlink_list",
add_url="extras:customlink_add", import_url="extras:customlink_import"), 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( MenuGroup(