diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index b4962dfd7b2..89398ee2b49 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -3,7 +3,7 @@ from netbox.admin import admin_site from utilities.forms import LaxURLField -from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, Webhook +from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook def order_content_types(field): @@ -77,6 +77,30 @@ def models(self, obj): return ', '.join([ct.name for ct in obj.obj_type.all()]) +# +# Custom links +# + +class CustomLinkForm(forms.ModelForm): + + class Meta: + model = CustomLink + exclude = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Format ContentType choices + order_content_types(self.fields['content_type']) + self.fields['content_type'].choices.insert(0, ('', '---------')) + + +@admin.register(CustomLink, site=admin_site) +class CustomLinkAdmin(admin.ModelAdmin): + list_display = ['name', 'content_type', 'group_name', 'weight'] + form = CustomLinkForm + + # # Graphs # diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 13c15cbba96..4daf3283966 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -35,6 +35,46 @@ (CF_FILTER_EXACT, 'Exact'), ) +# Custom links +CUSTOM_LINK_MODELS = [ + 'circuits.circuit', + 'circuits.provider', + 'dcim.cable', + 'dcim.device', + 'dcim.devicetype', + 'dcim.powerpanel', + 'dcim.powerfeed', + 'dcim.rack', + 'dcim.site', + 'ipam.aggregate', + 'ipam.ipaddress', + 'ipam.prefix', + 'ipam.service', + 'ipam.vlan', + 'ipam.vrf', + 'secrets.secret', + 'tenancy.tenant', + 'virtualization.cluster', + 'virtualization.virtualmachine', +] + +BUTTON_CLASS_DEFAULT = 'default' +BUTTON_CLASS_PRIMARY = 'primary' +BUTTON_CLASS_SUCCESS = 'success' +BUTTON_CLASS_INFO = 'info' +BUTTON_CLASS_WARNING = 'warning' +BUTTON_CLASS_DANGER = 'danger' +BUTTON_CLASS_LINK = 'link' +BUTTON_CLASS_CHOICES = ( + (BUTTON_CLASS_DEFAULT, 'Default'), + (BUTTON_CLASS_PRIMARY, 'Primary (blue)'), + (BUTTON_CLASS_SUCCESS, 'Success (green)'), + (BUTTON_CLASS_INFO, 'Info (aqua)'), + (BUTTON_CLASS_WARNING, 'Warning (orange)'), + (BUTTON_CLASS_DANGER, 'Danger (red)'), + (BUTTON_CLASS_LINK, 'None (link)'), +) + # Graph types GRAPH_TYPE_INTERFACE = 100 GRAPH_TYPE_PROVIDER = 200 diff --git a/netbox/extras/migrations/0022_custom_links.py b/netbox/extras/migrations/0022_custom_links.py new file mode 100644 index 00000000000..993dc0f4278 --- /dev/null +++ b/netbox/extras/migrations/0022_custom_links.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2 on 2019-04-15 19:28 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('extras', '0021_add_color_comments_changelog_to_tag'), + ] + + operations = [ + migrations.CreateModel( + name='CustomLink', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('text', models.CharField(max_length=200)), + ('url', models.CharField(max_length=200)), + ('weight', models.PositiveSmallIntegerField(default=100)), + ('group_name', models.CharField(blank=True, max_length=50)), + ('button_class', models.CharField(default='default', max_length=30)), + ('new_window', models.BooleanField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'ordering': ['group_name', 'weight', 'name'], + }, + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 042ba6e6044..0f151fb3971 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -300,6 +300,65 @@ def delete(self, using=None, keep_parents=False): CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete() +# +# Custom links +# + +def get_custom_link_models(): + # TODO: This should match on the app_label as well as the model name to avoid potential duplicate names + return { + 'model__in': [model.split('.')[1] for model in CUSTOM_LINK_MODELS], + } + + +class CustomLink(models.Model): + """ + 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. + """ + content_type = models.ForeignKey( + to=ContentType, + on_delete=models.CASCADE, + limit_choices_to=get_custom_link_models + ) + name = models.CharField( + max_length=100, + unique=True + ) + text = models.CharField( + max_length=200, + help_text="Jinja2 template code for link text" + ) + url = models.CharField( + max_length=200, + verbose_name='URL', + help_text="Jinja2 template code for link URL" + ) + weight = models.PositiveSmallIntegerField( + default=100 + ) + group_name = models.CharField( + max_length=50, + blank=True, + help_text="Links with the same group will appear as a dropdown menu" + ) + button_class = models.CharField( + max_length=30, + choices=BUTTON_CLASS_CHOICES, + default=BUTTON_CLASS_DEFAULT, + help_text="The class of the first link in a group will be used for the dropdown button" + ) + new_window = models.BooleanField( + help_text="Force link to open in a new window" + ) + + class Meta: + ordering = ['group_name', 'weight', 'name'] + + def __str__(self): + return self.name + + # # Graphs # diff --git a/netbox/extras/templatetags/__init__.py b/netbox/extras/templatetags/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py new file mode 100644 index 00000000000..193c465a5dd --- /dev/null +++ b/netbox/extras/templatetags/custom_links.py @@ -0,0 +1,68 @@ +from collections import OrderedDict + +from django import template +from django.contrib.contenttypes.models import ContentType +from django.utils.safestring import mark_safe +from jinja2 import Environment + +from extras.models import CustomLink + + +register = template.Library() + +LINK_BUTTON = '{}\n' +GROUP_BUTTON = '