mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Closes #8296: Allow disabling custom links
This commit is contained in:
parent
17aa37ae21
commit
72e17914e2
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
@ -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():
|
||||
|
@ -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()
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
netbox/extras/migrations/0070_customlink_enabled.py
Normal file
18
netbox/extras/migrations/0070_customlink_enabled.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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"
|
||||
|
@ -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')
|
||||
|
||||
|
||||
#
|
||||
|
@ -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 ''
|
||||
|
||||
|
@ -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',
|
||||
),
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user