diff --git a/docs/models/extras/customlink.md b/docs/models/extras/customlink.md index 7fd510841..96ff0bbf7 100644 --- a/docs/models/extras/customlink.md +++ b/docs/models/extras/customlink.md @@ -15,7 +15,7 @@ When viewing a device named Router4, this link would render as: View NMS ``` -Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links. +Custom links appear as buttons in the top right corner of the page. Numeric weighting can be used to influence the ordering of links, and each link can be enabled or disabled individually. !!! warning Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users. diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index e0ef639fa..31025bb85 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -64,6 +64,7 @@ Inventory item templates can be arranged hierarchically within a device type, an * [#7846](https://github.com/netbox-community/netbox/issues/7846) - Enable associating inventory items with device components * [#7852](https://github.com/netbox-community/netbox/issues/7852) - Enable assigning interfaces to VRFs * [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group +* [#8296](https://github.com/netbox-community/netbox/issues/8296) - Allow disabling custom links ### Other Changes @@ -106,6 +107,8 @@ Inventory item templates can be arranged hierarchically within a device type, an * Add `cluster_types` field * extras.CustomField * Added `object_type` field +* extras.CustomLink + * Added `enabled` field * ipam.VLANGroup * Added the `/availables-vlans/` endpoint * Added the `min_vid` and `max_vid` fields diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index fa0e5189f..6279ea2b7 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -101,7 +101,7 @@ class CustomLinkSerializer(ValidatedModelSerializer): class Meta: model = CustomLink fields = [ - 'id', 'url', 'display', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', + 'id', 'url', 'display', 'content_type', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window', ] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index bf25ff76c..a839e2dd3 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -82,7 +82,9 @@ class CustomLinkFilterSet(BaseFilterSet): class Meta: model = CustomLink - fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window'] + fields = [ + 'id', 'content_type', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', + ] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 1b87256a5..56b51c894 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -47,6 +47,10 @@ class CustomLinkBulkEditForm(BulkEditForm): limit_choices_to=FeatureQuery('custom_fields'), required=False ) + enabled = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) new_window = forms.NullBooleanField( required=False, widget=BulkEditNullBooleanSelect() diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 9f44494e0..fa6d8af55 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -51,7 +51,8 @@ class CustomLinkCSVForm(CSVModelForm): class Meta: model = CustomLink fields = ( - 'name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url', + 'name', 'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', + 'link_url', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 388cd1e60..330bb91e3 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -58,15 +58,18 @@ class CustomFieldFilterForm(FilterForm): class CustomLinkFilterForm(FilterForm): field_groups = [ ['q'], - ['content_type', 'weight', 'new_window'], + ['content_type', 'enabled', 'new_window', 'weight'], ] content_type = ContentTypeChoiceField( queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('custom_fields'), required=False ) - weight = forms.IntegerField( - required=False + enabled = forms.NullBooleanField( + required=False, + widget=StaticSelect( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) ) new_window = forms.NullBooleanField( required=False, @@ -74,6 +77,9 @@ class CustomLinkFilterForm(FilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + weight = forms.IntegerField( + required=False + ) class ExportTemplateFilterForm(FilterForm): diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py index 55e58a7f2..ca2c6b900 100644 --- a/netbox/extras/forms/models.py +++ b/netbox/extras/forms/models.py @@ -53,7 +53,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm): model = CustomLink fields = '__all__' fieldsets = ( - ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'new_window')), + ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), ('Templates', ('link_text', 'link_url')), ) widgets = { diff --git a/netbox/extras/migrations/0070_customlink_enabled.py b/netbox/extras/migrations/0070_customlink_enabled.py new file mode 100644 index 000000000..839a4dba5 --- /dev/null +++ b/netbox/extras/migrations/0070_customlink_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.11 on 2022-01-10 16:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0069_custom_object_field'), + ] + + operations = [ + migrations.AddField( + model_name='customlink', + name='enabled', + field=models.BooleanField(default=True), + ), + ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index ac3a23410..3612b2a6f 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -192,6 +192,9 @@ class CustomLink(ChangeLoggedModel): max_length=100, unique=True ) + enabled = models.BooleanField( + default=True + ) link_text = models.CharField( max_length=500, help_text="Jinja2 template code for link text" diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 071caa354..adfccb575 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -73,15 +73,16 @@ class CustomLinkTable(BaseTable): linkify=True ) content_type = ContentTypeColumn() + enabled = BooleanColumn() new_window = BooleanColumn() class Meta(BaseTable.Meta): model = CustomLink fields = ( - 'pk', 'id', 'name', 'content_type', 'link_text', 'link_url', 'weight', 'group_name', + 'pk', 'id', 'name', 'content_type', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window', ) - default_columns = ('pk', 'name', 'content_type', 'group_name', 'button_class', 'new_window') + default_columns = ('pk', 'name', 'content_type', 'enabled', 'group_name', 'button_class', 'new_window') # diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index 32ec966b3..dd5467338 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -36,7 +36,7 @@ def custom_links(context, obj): Render all applicable links for the given object. """ content_type = ContentType.objects.get_for_model(obj) - custom_links = CustomLink.objects.filter(content_type=content_type) + custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True) if not custom_links: return '' diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index d15b57e43..d790eff71 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -139,24 +139,28 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): { 'content_type': 'dcim.site', 'name': 'Custom Link 4', + 'enabled': True, 'link_text': 'Link 4', 'link_url': 'http://example.com/?4', }, { 'content_type': 'dcim.site', 'name': 'Custom Link 5', + 'enabled': True, 'link_text': 'Link 5', 'link_url': 'http://example.com/?5', }, { 'content_type': 'dcim.site', 'name': 'Custom Link 6', + 'enabled': False, 'link_text': 'Link 6', 'link_url': 'http://example.com/?6', }, ] bulk_update_data = { 'new_window': True, + 'enabled': False, } @classmethod @@ -167,18 +171,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): CustomLink( content_type=site_ct, name='Custom Link 1', + enabled=True, link_text='Link 1', link_url='http://example.com/?1', ), CustomLink( content_type=site_ct, name='Custom Link 2', + enabled=True, link_text='Link 2', link_url='http://example.com/?2', ), CustomLink( content_type=site_ct, name='Custom Link 3', + enabled=False, link_text='Link 3', link_url='http://example.com/?3', ), diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index a5f77afa9..3a08055cb 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -100,6 +100,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): CustomLink( name='Custom Link 1', content_type=content_types[0], + enabled=True, weight=100, new_window=False, link_text='Link 1', @@ -108,6 +109,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): CustomLink( name='Custom Link 2', content_type=content_types[1], + enabled=True, weight=200, new_window=False, link_text='Link 1', @@ -116,6 +118,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): CustomLink( name='Custom Link 3', content_type=content_types[2], + enabled=False, weight=300, new_window=True, link_text='Link 1', @@ -136,6 +139,12 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): params = {'weight': [100, 200]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_enabled(self): + params = {'enabled': True} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'enabled': False} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_new_window(self): params = {'new_window': False} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 34d5cb67e..ea3a952d6 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -59,14 +59,15 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): 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'), + 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 2', content_type=site_ct, 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'), )) cls.form_data = { 'name': 'Custom Link X', 'content_type': site_ct.pk, + 'enabled': False, 'weight': 100, 'button_class': CustomLinkButtonClassChoices.DEFAULT, 'link_text': 'Link X', @@ -74,14 +75,15 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "name,content_type,weight,button_class,link_text,link_url", - "Custom Link 4,dcim.site,100,blue,Link 4,http://exmaple.com/?4", - "Custom Link 5,dcim.site,100,blue,Link 5,http://exmaple.com/?5", - "Custom Link 6,dcim.site,100,blue,Link 6,http://exmaple.com/?6", + "name,content_type,enabled,weight,button_class,link_text,link_url", + "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 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6", ) cls.bulk_edit_data = { 'button_class': CustomLinkButtonClassChoices.CYAN, + 'enabled': False, 'weight': 200, } diff --git a/netbox/templates/extras/customlink.html b/netbox/templates/extras/customlink.html index ebf50882c..1f3866182 100644 --- a/netbox/templates/extras/customlink.html +++ b/netbox/templates/extras/customlink.html @@ -19,6 +19,10 @@