mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Add UI views for custom links
This commit is contained in:
parent
b017927c69
commit
276ded0119
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
#
|
||||
|
74
netbox/templates/extras/customlink.html
Normal file
74
netbox/templates/extras/customlink.html
Normal 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 %}
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user