Răsfoiți Sursa

Closes #8296: Allow disabling custom links

jeremystretch 4 ani în urmă
părinte
comite
72e17914e2

+ 1 - 1
docs/models/extras/customlink.md

@@ -15,7 +15,7 @@ When viewing a device named Router4, this link would render as:
 <a href="https://nms.example.com/nodes/?name=Router4">View NMS</a>
 ```
 
-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.

+ 3 - 0
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

+ 1 - 1
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',
         ]
 

+ 3 - 1
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():

+ 4 - 0
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()

+ 2 - 1
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',
         )
 
 

+ 9 - 3
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):

+ 1 - 1
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 = {

+ 18 - 0
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),
+        ),
+    ]

+ 3 - 0
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"

+ 3 - 2
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')
 
 
 #

+ 1 - 1
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 ''
 

+ 7 - 0
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',
             ),

+ 9 - 0
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)

+ 9 - 7
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,
         }
 

+ 4 - 0
netbox/templates/extras/customlink.html

@@ -19,6 +19,10 @@
             <th scope="row">Content Type</th>
             <td>{{ object.content_type }}</td>
           </tr>
+          <tr>
+            <th scope="row">Enabled</th>
+            <td>{% checkmark object.enabled %}</td>
+          </tr>
           <tr>
             <th scope="row">Group Name</th>
             <td>{{ object.group_name|placeholder }}</td>

+ 10 - 9
netbox/utilities/tables/tables.py

@@ -35,15 +35,16 @@ class BaseTable(tables.Table):
         if extra_columns is None:
             extra_columns = []
 
-        # Add custom field columns
-        obj_type = ContentType.objects.get_for_model(self._meta.model)
-        cf_columns = [
-            (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
-        ]
-        cl_columns = [
-            (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
-        ]
-        extra_columns.extend([*cf_columns, *cl_columns])
+        # Add custom field & custom link columns
+        content_type = ContentType.objects.get_for_model(self._meta.model)
+        custom_fields = CustomField.objects.filter(content_types=content_type)
+        extra_columns.extend([
+            (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
+        ])
+        custom_links = CustomLink.objects.filter(content_type=content_type, enabled=True)
+        extra_columns.extend([
+            (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links
+        ])
 
         super().__init__(*args, extra_columns=extra_columns, **kwargs)