{{ obj }}
. '
- 'Links which render as empty text will not be displayed.',
- 'link_url': 'Jinja2 template code for the link URL. Reference the object as {{ obj }}
.',
- }
-
-
-@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
#
diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py
index 4a0ce4416..afc32f8b6 100644
--- a/netbox/extras/forms.py
+++ b/netbox/extras/forms.py
@@ -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
#
diff --git a/netbox/extras/migrations/0061_extras_change_logging.py b/netbox/extras/migrations/0061_extras_change_logging.py
index 3081c7ddf..405855e54 100644
--- a/netbox/extras/migrations/0061_extras_change_logging.py
+++ b/netbox/extras/migrations/0061_extras_change_logging.py
@@ -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),
+ ),
]
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index ab9cbe9f3..b8e1acc81 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -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
diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py
index f6bb2f000..94ee6db64 100644
--- a/netbox/extras/tables.py
+++ b/netbox/extras/tables.py
@@ -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
#
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
index 41de01ee2..75c1bbbaa 100644
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -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
diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py
index 0e87277fb..64e6814eb 100644
--- a/netbox/extras/urls.py
+++ b/netbox/extras/urls.py
@@ -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/Content Type | +{{ object.content_type }} | +
---|---|
Name | +{{ object.name }} | +
Group Name | +{{ object.group_name|placeholder }} | +
Weight | +{{ object.weight }} | +
Button Class | +{{ object.get_button_class_display }} | +
New Window | ++ {% if object.new_window %} + + {% else %} + + {% endif %} + | +
{{ object.link_text }}+
{{ object.link_url }}+