mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 05:21:55 -06:00
Merge pull request #10760 from netbox-community/8274-customlink-content-types
Closes #8274: Enable associating a custom link with multiple object types
This commit is contained in:
commit
7a155407f6
@ -8,6 +8,7 @@
|
|||||||
* Device and virtual machine names are no longer case-sensitive. Attempting to create e.g. "device1" and "DEVICE1" will raise a validation error.
|
* Device and virtual machine names are no longer case-sensitive. Attempting to create e.g. "device1" and "DEVICE1" will raise a validation error.
|
||||||
* The `asn` field has been removed from the provider model. Please replicate any provider ASN assignments to the ASN model introduced in NetBox v3.1 prior to upgrading.
|
* The `asn` field has been removed from the provider model. Please replicate any provider ASN assignments to the ASN model introduced in NetBox v3.1 prior to upgrading.
|
||||||
* The `noc_contact`, `admin_contact`, and `portal_url` fields have been removed from the provider model. Please replicate any data remaining in these fields to the contact model introduced in NetBox v3.1 prior to upgrading.
|
* The `noc_contact`, `admin_contact`, and `portal_url` fields have been removed from the provider model. Please replicate any data remaining in these fields to the contact model introduced in NetBox v3.1 prior to upgrading.
|
||||||
|
* The `content_type` field on the CustomLink model has been renamed to `content_types` and now supports the assignment of multiple content types.
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects
|
* [#8245](https://github.com/netbox-community/netbox/issues/8245) - Enable GraphQL filtering of related objects
|
||||||
|
* [#8274](https://github.com/netbox-community/netbox/issues/8274) - Enable associating a custom link with multiple object types
|
||||||
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
|
* [#9249](https://github.com/netbox-community/netbox/issues/9249) - Device and virtual machine names are no longer case-sensitive
|
||||||
* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
|
* [#9478](https://github.com/netbox-community/netbox/issues/9478) - Add `link_peers` field to GraphQL types for cabled objects
|
||||||
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
|
* [#9654](https://github.com/netbox-community/netbox/issues/9654) - Add `weight` field to racks, device types, and module types
|
||||||
@ -57,6 +59,8 @@ A new `PluginMenu` class has been introduced, which enables a plugin to inject a
|
|||||||
* Added optional `weight` and `weight_unit` fields
|
* Added optional `weight` and `weight_unit` fields
|
||||||
* dcim.Rack
|
* dcim.Rack
|
||||||
* Added optional `weight` and `weight_unit` fields
|
* Added optional `weight` and `weight_unit` fields
|
||||||
|
* extras.CustomLink
|
||||||
|
* Renamed `content_type` field to `content_types`
|
||||||
* ipam.FHRPGroup
|
* ipam.FHRPGroup
|
||||||
* Added optional `name` field
|
* Added optional `name` field
|
||||||
|
|
||||||
|
@ -117,14 +117,15 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class CustomLinkSerializer(ValidatedModelSerializer):
|
class CustomLinkSerializer(ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
|
||||||
content_type = ContentTypeField(
|
content_types = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query())
|
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
|
||||||
|
many=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_type', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
|
||||||
'button_class', 'new_window', 'created', 'last_updated',
|
'button_class', 'new_window', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -93,11 +93,15 @@ class CustomLinkFilterSet(BaseFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
content_type_id = MultiValueNumberFilter(
|
||||||
|
field_name='content_types__id'
|
||||||
|
)
|
||||||
|
content_types = ContentTypeFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'content_type', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window',
|
'id', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window',
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
|
@ -53,11 +53,6 @@ class CustomLinkBulkEditForm(BulkEditForm):
|
|||||||
queryset=CustomLink.objects.all(),
|
queryset=CustomLink.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
)
|
)
|
||||||
content_type = ContentTypeChoiceField(
|
|
||||||
queryset=ContentType.objects.all(),
|
|
||||||
limit_choices_to=FeatureQuery('custom_links'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
enabled = forms.NullBooleanField(
|
enabled = forms.NullBooleanField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=BulkEditNullBooleanSelect()
|
widget=BulkEditNullBooleanSelect()
|
||||||
|
@ -53,16 +53,16 @@ class CustomFieldCSVForm(CSVModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class CustomLinkCSVForm(CSVModelForm):
|
class CustomLinkCSVForm(CSVModelForm):
|
||||||
content_type = CSVContentTypeField(
|
content_types = CSVMultipleContentTypeField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_links'),
|
limit_choices_to=FeatureQuery('custom_links'),
|
||||||
help_text="Assigned object type"
|
help_text="One or more assigned object types"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text',
|
'name', 'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text',
|
||||||
'link_url',
|
'link_url',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -121,9 +121,9 @@ class JobResultFilterForm(FilterForm):
|
|||||||
class CustomLinkFilterForm(FilterForm):
|
class CustomLinkFilterForm(FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q',)),
|
||||||
('Attributes', ('content_type', 'enabled', 'new_window', 'weight')),
|
('Attributes', ('content_types', 'enabled', 'new_window', 'weight')),
|
||||||
)
|
)
|
||||||
content_type = ContentTypeChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_links'),
|
limit_choices_to=FeatureQuery('custom_links'),
|
||||||
required=False
|
required=False
|
||||||
|
@ -63,13 +63,13 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
|
||||||
content_type = ContentTypeChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
limit_choices_to=FeatureQuery('custom_links')
|
limit_choices_to=FeatureQuery('custom_links')
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
|
('Custom Link', ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
|
||||||
('Templates', ('link_text', 'link_url')),
|
('Templates', ('link_text', 'link_url')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class CustomLinkType(ObjectType):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.CustomLink
|
model = models.CustomLink
|
||||||
fields = '__all__'
|
exclude = ('content_types', )
|
||||||
filterset_class = filtersets.CustomLinkFilterSet
|
filterset_class = filtersets.CustomLinkFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
32
netbox/extras/migrations/0081_customlink_content_types.py
Normal file
32
netbox/extras/migrations/0081_customlink_content_types.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def copy_content_types(apps, schema_editor):
|
||||||
|
CustomLink = apps.get_model('extras', 'CustomLink')
|
||||||
|
|
||||||
|
for customlink in CustomLink.objects.all():
|
||||||
|
customlink.content_types.set([customlink.content_type])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0080_search'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='content_types',
|
||||||
|
field=models.ManyToManyField(related_name='custom_links', to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=copy_content_types,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='customlink',
|
||||||
|
name='content_type',
|
||||||
|
),
|
||||||
|
]
|
@ -197,10 +197,10 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
|
|||||||
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
|
||||||
code to be rendered with an object as context.
|
code to be rendered with an object as context.
|
||||||
"""
|
"""
|
||||||
content_type = models.ForeignKey(
|
content_types = models.ManyToManyField(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.CASCADE,
|
related_name='custom_links',
|
||||||
limit_choices_to=FeatureQuery('custom_links')
|
help_text='The object type(s) to which this link applies.'
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
@ -236,7 +236,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogged
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -3,7 +3,6 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from extras.models import CustomLink
|
from extras.models import CustomLink
|
||||||
from utilities.utils import render_jinja2
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
@ -34,7 +33,7 @@ def custom_links(context, obj):
|
|||||||
Render all applicable links for the given object.
|
Render all applicable links for the given object.
|
||||||
"""
|
"""
|
||||||
content_type = ContentType.objects.get_for_model(obj)
|
content_type = ContentType.objects.get_for_model(obj)
|
||||||
custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True)
|
custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True)
|
||||||
if not custom_links:
|
if not custom_links:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -137,21 +137,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['display', 'id', 'name', 'url']
|
||||||
create_data = [
|
create_data = [
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 4',
|
'name': 'Custom Link 4',
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'link_text': 'Link 4',
|
'link_text': 'Link 4',
|
||||||
'link_url': 'http://example.com/?4',
|
'link_url': 'http://example.com/?4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 5',
|
'name': 'Custom Link 5',
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
'link_text': 'Link 5',
|
'link_text': 'Link 5',
|
||||||
'link_url': 'http://example.com/?5',
|
'link_url': 'http://example.com/?5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'content_type': 'dcim.site',
|
'content_types': ['dcim.site'],
|
||||||
'name': 'Custom Link 6',
|
'name': 'Custom Link 6',
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'link_text': 'Link 6',
|
'link_text': 'Link 6',
|
||||||
@ -169,21 +169,18 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
custom_links = (
|
custom_links = (
|
||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
|
||||||
name='Custom Link 1',
|
name='Custom Link 1',
|
||||||
enabled=True,
|
enabled=True,
|
||||||
link_text='Link 1',
|
link_text='Link 1',
|
||||||
link_url='http://example.com/?1',
|
link_url='http://example.com/?1',
|
||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
|
||||||
name='Custom Link 2',
|
name='Custom Link 2',
|
||||||
enabled=True,
|
enabled=True,
|
||||||
link_text='Link 2',
|
link_text='Link 2',
|
||||||
link_url='http://example.com/?2',
|
link_url='http://example.com/?2',
|
||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
content_type=site_ct,
|
|
||||||
name='Custom Link 3',
|
name='Custom Link 3',
|
||||||
enabled=False,
|
enabled=False,
|
||||||
link_text='Link 3',
|
link_text='Link 3',
|
||||||
@ -191,6 +188,8 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
CustomLink.objects.bulk_create(custom_links)
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
|
for i, custom_link in enumerate(custom_links):
|
||||||
|
custom_link.content_types.set([site_ct])
|
||||||
|
|
||||||
|
|
||||||
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||||
|
@ -168,7 +168,6 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
custom_links = (
|
custom_links = (
|
||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 1',
|
name='Custom Link 1',
|
||||||
content_type=content_types[0],
|
|
||||||
enabled=True,
|
enabled=True,
|
||||||
weight=100,
|
weight=100,
|
||||||
new_window=False,
|
new_window=False,
|
||||||
@ -177,7 +176,6 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 2',
|
name='Custom Link 2',
|
||||||
content_type=content_types[1],
|
|
||||||
enabled=True,
|
enabled=True,
|
||||||
weight=200,
|
weight=200,
|
||||||
new_window=False,
|
new_window=False,
|
||||||
@ -186,7 +184,6 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
),
|
),
|
||||||
CustomLink(
|
CustomLink(
|
||||||
name='Custom Link 3',
|
name='Custom Link 3',
|
||||||
content_type=content_types[2],
|
|
||||||
enabled=False,
|
enabled=False,
|
||||||
weight=300,
|
weight=300,
|
||||||
new_window=True,
|
new_window=True,
|
||||||
@ -195,13 +192,17 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
CustomLink.objects.bulk_create(custom_links)
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
|
for i, custom_link in enumerate(custom_links):
|
||||||
|
custom_link.content_types.set([content_types[i]])
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_content_type(self):
|
def test_content_types(self):
|
||||||
params = {'content_type': ContentType.objects.get(model='site').pk}
|
params = {'content_types': 'dcim.site'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_weight(self):
|
def test_weight(self):
|
||||||
|
@ -59,17 +59,19 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
site_ct = ContentType.objects.get_for_model(Site)
|
site_ct = ContentType.objects.get_for_model(Site)
|
||||||
CustomLink.objects.bulk_create((
|
custom_links = (
|
||||||
CustomLink(name='Custom Link 1', content_type=site_ct, enabled=True, link_text='Link 1', link_url='http://example.com/?1'),
|
CustomLink(name='Custom Link 1', enabled=True, link_text='Link 1', link_url='http://example.com/?1'),
|
||||||
CustomLink(name='Custom Link 2', content_type=site_ct, enabled=True, link_text='Link 2', link_url='http://example.com/?2'),
|
CustomLink(name='Custom Link 2', enabled=True, link_text='Link 2', link_url='http://example.com/?2'),
|
||||||
CustomLink(name='Custom Link 3', content_type=site_ct, enabled=False, link_text='Link 3', link_url='http://example.com/?3'),
|
CustomLink(name='Custom Link 3', enabled=False, link_text='Link 3', link_url='http://example.com/?3'),
|
||||||
))
|
)
|
||||||
|
CustomLink.objects.bulk_create(custom_links)
|
||||||
|
for i, custom_link in enumerate(custom_links):
|
||||||
|
custom_link.content_types.set([site_ct])
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Custom Link X',
|
'name': 'Custom Link X',
|
||||||
'content_type': site_ct.pk,
|
'content_types': [site_ct.pk],
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'weight': 100,
|
'weight': 100,
|
||||||
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
||||||
@ -78,7 +80,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,content_type,enabled,weight,button_class,link_text,link_url",
|
"name,content_types,enabled,weight,button_class,link_text,link_url",
|
||||||
"Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4",
|
"Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4",
|
||||||
"Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5",
|
"Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5",
|
||||||
"Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6",
|
"Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6",
|
||||||
@ -327,13 +329,13 @@ class CustomLinkTest(TestCase):
|
|||||||
|
|
||||||
def test_view_object_with_custom_link(self):
|
def test_view_object_with_custom_link(self):
|
||||||
customlink = CustomLink(
|
customlink = CustomLink(
|
||||||
content_type=ContentType.objects.get_for_model(Site),
|
|
||||||
name='Test',
|
name='Test',
|
||||||
link_text='FOO {{ obj.name }} BAR',
|
link_text='FOO {{ obj.name }} BAR',
|
||||||
link_url='http://example.com/?site={{ obj.slug }}',
|
link_url='http://example.com/?site={{ obj.slug }}',
|
||||||
new_window=False
|
new_window=False
|
||||||
)
|
)
|
||||||
customlink.save()
|
customlink.save()
|
||||||
|
customlink.content_types.set([ContentType.objects.get_for_model(Site)])
|
||||||
|
|
||||||
site = Site(name='Test Site', slug='test-site')
|
site = Site(name='Test Site', slug='test-site')
|
||||||
site.save()
|
site.save()
|
||||||
|
@ -191,7 +191,7 @@ class NetBoxTable(BaseTable):
|
|||||||
extra_columns.extend([
|
extra_columns.extend([
|
||||||
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
|
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
|
||||||
])
|
])
|
||||||
custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True)
|
custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True)
|
||||||
extra_columns.extend([
|
extra_columns.extend([
|
||||||
(f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links
|
(f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links
|
||||||
])
|
])
|
||||||
|
@ -6,19 +6,13 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-5">
|
<div class="col col-md-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Custom Link</h5>
|
||||||
Custom Link
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Name</th>
|
<th scope="row">Name</th>
|
||||||
<td>{{ object.name }}</td>
|
<td>{{ object.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">Content Type</th>
|
|
||||||
<td>{{ object.content_type }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Enabled</th>
|
<th scope="row">Enabled</th>
|
||||||
<td>{% checkmark object.enabled %}</td>
|
<td>{% checkmark object.enabled %}</td>
|
||||||
@ -42,6 +36,18 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Assigned Models</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
{% for ct in object.content_types.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ct }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-7">
|
<div class="col col-md-7">
|
||||||
|
Loading…
Reference in New Issue
Block a user