Add UI views for custom links

This commit is contained in:
jeremystretch 2021-06-23 17:09:15 -04:00
parent b017927c69
commit 276ded0119
10 changed files with 292 additions and 60 deletions

View File

@ -57,52 +57,6 @@ class WebhookAdmin(admin.ModelAdmin):
return ', '.join([ct.name for ct in obj.content_types.all()])
#
# Custom links
#
class CustomLinkForm(forms.ModelForm):
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links')
)
class Meta:
model = CustomLink
exclude = []
widgets = {
'link_text': forms.Textarea,
'link_url': forms.Textarea,
}
help_texts = {
'weight': 'A numeric weight to influence the ordering of this link among its peers. Lower weights appear '
'first in a list.',
'link_text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>. '
'Links which render as empty text will not be displayed.',
'link_url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
}
@admin.register(CustomLink)
class CustomLinkAdmin(admin.ModelAdmin):
fieldsets = (
('Custom Link', {
'fields': ('content_type', 'name', 'group_name', 'weight', 'button_class', 'new_window')
}),
('Templates', {
'fields': ('link_text', 'link_url'),
'classes': ('monospace',)
})
)
list_display = [
'name', 'content_type', 'group_name', 'weight',
]
list_filter = [
'content_type',
]
form = CustomLinkForm
#
# Export templates
#

View File

@ -8,13 +8,13 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField,
CommentField, ContentTypeMultipleChoiceField, CSVModelForm, CSVMultipleContentTypeField, DateTimePicker,
DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2, StaticSelect2Multiple,
BOOLEAN_WITH_BLANK_CHOICES,
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, CSVContentTypeField, CSVModelForm,
CSVMultipleContentTypeField, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup
from .choices import *
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
from .models import *
from .utils import FeatureQuery
@ -100,6 +100,86 @@ class CustomFieldFilterForm(BootstrapMixin, forms.Form):
)
#
# Custom links
#
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links')
)
class Meta:
model = CustomLink
fields = '__all__'
fieldsets = (
('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')),
('Templates', ('link_text', 'link_url')),
)
class CustomLinkCSVForm(CSVModelForm):
content_type = CSVContentTypeField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links'),
help_text="One or more assigned object types"
)
class Meta:
model = CustomLink
fields = (
'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url',
)
class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=CustomLink.objects.all(),
widget=forms.MultipleHiddenInput
)
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
required=False
)
new_window = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
weight = forms.IntegerField(
required=False
)
button_class = forms.ChoiceField(
choices=CustomLinkButtonClassChoices,
required=False,
widget=StaticSelect2()
)
class Meta:
nullable_fields = []
class CustomLinkFilterForm(BootstrapMixin, forms.Form):
field_groups = [
['content_type'],
['weight', 'new_window'],
]
content_type = ContentTypeChoiceField(
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields')
)
weight = forms.IntegerField(
required=False
)
new_window = forms.NullBooleanField(
required=False,
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
#
# Custom field models
#

View File

@ -1,5 +1,3 @@
# Generated by Django 3.2.4 on 2021-06-23 17:37
from django.db import migrations, models
@ -20,4 +18,14 @@ class Migration(migrations.Migration):
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
migrations.AddField(
model_name='customlink',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='customlink',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@ -171,7 +171,7 @@ class Webhook(BigIDModel):
# Custom links
#
class CustomLink(BigIDModel):
class CustomLink(ChangeLoggedModel):
"""
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
code to be rendered with an object as context.
@ -221,6 +221,9 @@ class CustomLink(BigIDModel):
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('extras:customlink', args=[self.pk])
#
# Export templates

View File

@ -46,6 +46,24 @@ class CustomFieldTable(BaseTable):
default_columns = ('pk', 'name', 'label', 'type', 'required', 'description')
#
# Custom links
#
class CustomLinkTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
class Meta(BaseTable.Meta):
model = CustomLink
fields = (
'pk', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window',
)
default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window')
#
# Tags
#

View File

@ -6,7 +6,7 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from dcim.models import Site
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, ObjectChangeActionChoices
from extras.choices import *
from extras.models import *
from utilities.testing import ViewTestCases, TestCase
@ -51,6 +51,41 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = CustomLink
@classmethod
def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site)
CustomLink.objects.bulk_create((
CustomLink(name='Custom Link 1', content_type=site_ct, link_text='Link 1', link_url='http://example.com/?1'),
CustomLink(name='Custom Link 2', content_type=site_ct, link_text='Link 2', link_url='http://example.com/?2'),
CustomLink(name='Custom Link 3', content_type=site_ct, link_text='Link 3', link_url='http://example.com/?3'),
))
cls.form_data = {
'name': 'Custom Link X',
'content_type': site_ct.pk,
'weight': 100,
'button_class': CustomLinkButtonClassChoices.CLASS_DEFAULT,
'link_text': 'Link X',
'link_url': 'http://example.com/?x'
}
cls.csv_data = (
"name,content_type,weight,button_class,link_text,link_url",
"Custom Link 4,dcim.site,100,primary,Link 4,http://exmaple.com/?4",
"Custom Link 5,dcim.site,100,primary,Link 5,http://exmaple.com/?5",
"Custom Link 6,dcim.site,100,primary,Link 6,http://exmaple.com/?6",
)
cls.bulk_edit_data = {
'button_class': CustomLinkButtonClassChoices.CLASS_INFO,
'weight': 200,
}
class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Tag

View File

@ -1,7 +1,6 @@
from django.urls import path
from extras import views
from extras.models import ConfigContext, CustomField, JournalEntry, Tag
from extras import models, views
app_name = 'extras'
@ -16,7 +15,20 @@ urlpatterns = [
path('custom-fields/<int:pk>/', views.CustomFieldView.as_view(), name='customfield'),
path('custom-fields/<int:pk>/edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'),
path('custom-fields/<int:pk>/delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'),
path('custom-fields/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customfield_changelog', kwargs={'model': CustomField}),
path('custom-fields/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customfield_changelog',
kwargs={'model': models.CustomField}),
# Custom links
path('custom-links/', views.CustomLinkListView.as_view(), name='customlink_list'),
path('custom-links/add/', views.CustomLinkEditView.as_view(), name='customlink_add'),
path('custom-links/import/', views.CustomLinkBulkImportView.as_view(), name='customlink_import'),
path('custom-links/edit/', views.CustomLinkBulkEditView.as_view(), name='customlink_bulk_edit'),
path('custom-links/delete/', views.CustomLinkBulkDeleteView.as_view(), name='customlink_bulk_delete'),
path('custom-links/<int:pk>/', views.CustomLinkView.as_view(), name='customlink'),
path('custom-links/<int:pk>/edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'),
path('custom-links/<int:pk>/delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'),
path('custom-links/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customlink_changelog',
kwargs={'model': models.CustomLink}),
# Tags
path('tags/', views.TagListView.as_view(), name='tag_list'),
@ -27,7 +39,8 @@ urlpatterns = [
path('tags/<int:pk>/', views.TagView.as_view(), name='tag'),
path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path('tags/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
path('tags/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog',
kwargs={'model': models.Tag}),
# Config contexts
path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
@ -37,7 +50,8 @@ urlpatterns = [
path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
path('config-contexts/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='configcontext_changelog', kwargs={'model': ConfigContext}),
path('config-contexts/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='configcontext_changelog',
kwargs={'model': models.ConfigContext}),
# Image attachments
path('image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
@ -51,7 +65,8 @@ urlpatterns = [
path('journal-entries/<int:pk>/', views.JournalEntryView.as_view(), name='journalentry'),
path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'),
path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'),
path('journal-entries/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='journalentry_changelog', kwargs={'model': JournalEntry}),
path('journal-entries/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='journalentry_changelog',
kwargs={'model': models.JournalEntry}),
# Change logging
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),

View File

@ -63,6 +63,49 @@ class CustomFieldBulkDeleteView(generic.BulkDeleteView):
table = tables.CustomFieldTable
#
# Custom links
#
class CustomLinkListView(generic.ObjectListView):
queryset = CustomLink.objects.all()
filterset = filtersets.CustomLinkFilterSet
filterset_form = forms.CustomLinkFilterForm
table = tables.CustomLinkTable
class CustomLinkView(generic.ObjectView):
queryset = CustomLink.objects.all()
class CustomLinkEditView(generic.ObjectEditView):
queryset = CustomLink.objects.all()
model_form = forms.CustomLinkForm
class CustomLinkDeleteView(generic.ObjectDeleteView):
queryset = CustomLink.objects.all()
class CustomLinkBulkImportView(generic.BulkImportView):
queryset = CustomLink.objects.all()
model_form = forms.CustomLinkCSVForm
table = tables.CustomLinkTable
class CustomLinkBulkEditView(generic.BulkEditView):
queryset = CustomLink.objects.all()
filterset = filtersets.CustomLinkFilterSet
table = tables.CustomLinkTable
form = forms.CustomLinkBulkEditForm
class CustomLinkBulkDeleteView(generic.BulkDeleteView):
queryset = CustomLink.objects.all()
filterset = filtersets.CustomLinkFilterSet
table = tables.CustomLinkTable
#
# Tags
#

View File

@ -0,0 +1,74 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'extras:customlink_list' %}">Cusotm Links</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">
Custom Link
</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">Group Name</th>
<td>{{ object.group_name|placeholder }}</td>
</tr>
<tr>
<th scope="row">Weight</th>
<td>{{ object.weight }}</td>
</tr>
<tr>
<th scope="row">Button Class</th>
<td>{{ object.get_button_class_display }}</td>
</tr>
<tr>
<th scope="row">New Window</th>
<td>
{% if object.new_window %}
<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">
Link Text
</h5>
<div class="card-body">
<pre>{{ object.link_text }}</pre>
</div>
</div>
<div class="card">
<h5 class="card-header">
Link URL
</h5>
<div class="card-body">
<pre>{{ object.link_url }}</pre>
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
{% endblock %}

View File

@ -294,6 +294,8 @@ OTHER_MENU = Menu(
items=(
MenuItem(label="Custom Fields", url="extras:customfield_list",
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"),
),
),
MenuGroup(