Przeglądaj źródła

Introduce ButtonsColumn to reduce boilerplate and standardize organizational object links

Jeremy Stretch 5 lat temu
rodzic
commit
c484fa99e2

+ 2 - 16
netbox/circuits/tables.py

@@ -2,19 +2,9 @@ import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from tenancy.tables import COL_TENANT
 from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
 from .models import Circuit, CircuitType, Provider
 from .models import Circuit, CircuitType, Provider
 
 
-CIRCUITTYPE_ACTIONS = """
-<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.circuit.change_circuittype %}
-    <a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}"
-      class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 STATUS_LABEL = """
 STATUS_LABEL = """
 <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
 <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
 """
 """
@@ -53,11 +43,7 @@ class CircuitTypeTable(BaseTable):
     circuit_count = tables.Column(
     circuit_count = tables.Column(
         verbose_name='Circuits'
         verbose_name='Circuits'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=CIRCUITTYPE_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(CircuitType, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = CircuitType
         model = CircuitType

+ 14 - 98
netbox/dcim/tables.py

@@ -2,7 +2,9 @@ import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from tenancy.tables import COL_TENANT
 from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ColoredLabelColumn, TagColumn, ToggleColumn
+from utilities.tables import (
+    BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ColoredLabelColumn, TagColumn, ToggleColumn,
+)
 from .models import (
 from .models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@@ -40,69 +42,16 @@ DEVICE_LINK = """
 </a>
 </a>
 """
 """
 
 
-REGION_ACTIONS = """
-<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.dcim.change_region %}
-    <a href="{% url 'dcim:region_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
-RACKGROUP_ACTIONS = """
-<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
+RACKGROUP_ELEVATIONS = """
 <a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
 <a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
     <i class="fa fa-eye"></i>
     <i class="fa fa-eye"></i>
 </a>
 </a>
-{% if perms.dcim.change_rackgroup %}
-    <a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning" title="Edit">
-        <i class="glyphicon glyphicon-pencil"></i>
-    </a>
-{% endif %}
-"""
-
-RACKROLE_ACTIONS = """
-<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.dcim.change_rackrole %}
-    <a href="{% url 'dcim:rackrole_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
 """
 """
 
 
 RACK_DEVICE_COUNT = """
 RACK_DEVICE_COUNT = """
 <a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
 <a href="{% url 'dcim:device_list' %}?rack_id={{ record.pk }}">{{ value }}</a>
 """
 """
 
 
-RACKRESERVATION_ACTIONS = """
-<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.dcim.change_rackreservation %}
-    <a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
-MANUFACTURER_ACTIONS = """
-<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.dcim.change_manufacturer %}
-    <a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
-DEVICEROLE_ACTIONS = """
-<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.dcim.change_devicerole %}
-    <a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 DEVICEROLE_DEVICE_COUNT = """
 DEVICEROLE_DEVICE_COUNT = """
 <a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a>
 <a href="{% url 'dcim:device_list' %}?role={{ record.slug }}">{{ value }}</a>
 """
 """
@@ -119,15 +68,6 @@ PLATFORM_VM_COUNT = """
 <a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value }}</a>
 <a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ record.slug }}">{{ value }}</a>
 """
 """
 
 
-PLATFORM_ACTIONS = """
-<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.dcim.change_platform %}
-    <a href="{% url 'dcim:platform_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 STATUS_LABEL = """
 STATUS_LABEL = """
 <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
 <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
 """
 """
@@ -198,11 +138,7 @@ class RegionTable(BaseTable):
     site_count = tables.Column(
     site_count = tables.Column(
         verbose_name='Sites'
         verbose_name='Sites'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=REGION_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(Region)
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Region
         model = Region
@@ -260,10 +196,9 @@ class RackGroupTable(BaseTable):
     rack_count = tables.Column(
     rack_count = tables.Column(
         verbose_name='Racks'
         verbose_name='Racks'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=RACKGROUP_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=RackGroup,
+        prepend_template=RACKGROUP_ELEVATIONS
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -280,11 +215,7 @@ class RackRoleTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     rack_count = tables.Column(verbose_name='Racks')
     rack_count = tables.Column(verbose_name='Racks')
     color = tables.TemplateColumn(COLOR_LABEL)
     color = tables.TemplateColumn(COLOR_LABEL)
-    actions = tables.TemplateColumn(
-        template_code=RACKROLE_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(RackRole)
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = RackRole
         model = RackRole
@@ -386,11 +317,7 @@ class RackReservationTable(BaseTable):
     tags = TagColumn(
     tags = TagColumn(
         url_name='dcim:rackreservation_list'
         url_name='dcim:rackreservation_list'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=RACKRESERVATION_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(RackReservation)
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = RackReservation
         model = RackReservation
@@ -420,11 +347,7 @@ class ManufacturerTable(BaseTable):
         verbose_name='Platforms'
         verbose_name='Platforms'
     )
     )
     slug = tables.Column()
     slug = tables.Column()
-    actions = tables.TemplateColumn(
-        template_code=MANUFACTURER_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(Manufacturer, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Manufacturer
         model = Manufacturer
@@ -609,11 +532,8 @@ class DeviceRoleTable(BaseTable):
         template_code=COLOR_LABEL,
         template_code=COLOR_LABEL,
         verbose_name='Label'
         verbose_name='Label'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=DEVICEROLE_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    vm_role = BooleanColumn()
+    actions = ButtonsColumn(DeviceRole, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = DeviceRole
         model = DeviceRole
@@ -639,11 +559,7 @@ class PlatformTable(BaseTable):
         orderable=False,
         orderable=False,
         verbose_name='VMs'
         verbose_name='VMs'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=PLATFORM_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(Platform, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Platform
         model = Platform

+ 2 - 18
netbox/extras/tables.py

@@ -1,21 +1,9 @@
 import django_tables2 as tables
 import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
-from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
+from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, ToggleColumn
 from .models import ConfigContext, ObjectChange, Tag, TaggedItem
 from .models import ConfigContext, ObjectChange, Tag, TaggedItem
 
 
-TAG_ACTIONS = """
-<a href="{% url 'extras:tag_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.taggit.change_tag %}
-    <a href="{% url 'extras:tag_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-{% if perms.taggit.delete_tag %}
-    <a href="{% url 'extras:tag_delete' slug=record.slug %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-trash" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 TAGGED_ITEM = """
 TAGGED_ITEM = """
 {% if value.get_absolute_url %}
 {% if value.get_absolute_url %}
     <a href="{{ value.get_absolute_url }}">{{ value }}</a>
     <a href="{{ value.get_absolute_url }}">{{ value }}</a>
@@ -68,12 +56,8 @@ class TagTable(BaseTable):
         viewname='extras:tag',
         viewname='extras:tag',
         args=[Accessor('slug')]
         args=[Accessor('slug')]
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=TAG_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
     color = ColorColumn()
     color = ColorColumn()
+    actions = ButtonsColumn(Tag, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Tag
         model = Tag

+ 7 - 40
netbox/ipam/tables.py

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
 
 
 from dcim.models import Interface
 from dcim.models import Interface
 from tenancy.tables import COL_TENANT
 from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 RIR_UTILIZATION = """
 RIR_UTILIZATION = """
@@ -25,15 +25,6 @@ RIR_UTILIZATION = """
 </div>
 </div>
 """
 """
 
 
-RIR_ACTIONS = """
-<a href="{% url 'ipam:rir_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.ipam.change_rir %}
-    <a href="{% url 'ipam:rir_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 UTILIZATION_GRAPH = """
 UTILIZATION_GRAPH = """
 {% load helpers %}
 {% load helpers %}
 {% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %}
 {% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %}
@@ -47,15 +38,6 @@ ROLE_VLAN_COUNT = """
 <a href="{% url 'ipam:vlan_list' %}?role={{ record.slug }}">{{ value }}</a>
 <a href="{% url 'ipam:vlan_list' %}?role={{ record.slug }}">{{ value }}</a>
 """
 """
 
 
-ROLE_ACTIONS = """
-<a href="{% url 'ipam:role_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.ipam.change_role %}
-    <a href="{% url 'ipam:role_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 PREFIX_LINK = """
 PREFIX_LINK = """
 {% if record.has_children %}
 {% if record.has_children %}
     <span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
     <span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
@@ -136,10 +118,7 @@ VLAN_ROLE_LINK = """
 {% endif %}
 {% endif %}
 """
 """
 
 
-VLANGROUP_ACTIONS = """
-<a href="{% url 'ipam:vlangroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
+VLANGROUP_ADD_VLAN = """
 {% with next_vid=record.get_next_available_vid %}
 {% with next_vid=record.get_next_available_vid %}
     {% if next_vid and perms.ipam.add_vlan %}
     {% if next_vid and perms.ipam.add_vlan %}
         <a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
         <a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
@@ -147,9 +126,6 @@ VLANGROUP_ACTIONS = """
         </a>
         </a>
     {% endif %}
     {% endif %}
 {% endwith %}
 {% endwith %}
-{% if perms.ipam.change_vlangroup %}
-    <a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
 """
 """
 
 
 VLAN_MEMBER_UNTAGGED = """
 VLAN_MEMBER_UNTAGGED = """
@@ -214,11 +190,7 @@ class RIRTable(BaseTable):
     aggregate_count = tables.Column(
     aggregate_count = tables.Column(
         verbose_name='Aggregates'
         verbose_name='Aggregates'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=RIR_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(RIR, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = RIR
         model = RIR
@@ -322,11 +294,7 @@ class RoleTable(BaseTable):
         orderable=False,
         orderable=False,
         verbose_name='VLANs'
         verbose_name='VLANs'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=ROLE_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(Role, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Role
         model = Role
@@ -516,10 +484,9 @@ class VLANGroupTable(BaseTable):
     vlan_count = tables.Column(
     vlan_count = tables.Column(
         verbose_name='VLANs'
         verbose_name='VLANs'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=VLANGROUP_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
+    actions = ButtonsColumn(
+        model=VLANGroup,
+        prepend_template=VLANGROUP_ADD_VLAN
     )
     )
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):

+ 2 - 15
netbox/secrets/tables.py

@@ -1,17 +1,8 @@
 import django_tables2 as tables
 import django_tables2 as tables
 
 
-from utilities.tables import BaseTable, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
 from .models import SecretRole, Secret
 from .models import SecretRole, Secret
 
 
-SECRETROLE_ACTIONS = """
-<a href="{% url 'secrets:secretrole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.secrets.change_secretrole %}
-    <a href="{% url 'secrets:secretrole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 
 
 #
 #
 # Secret roles
 # Secret roles
@@ -23,11 +14,7 @@ class SecretRoleTable(BaseTable):
     secret_count = tables.Column(
     secret_count = tables.Column(
         verbose_name='Secrets'
         verbose_name='Secrets'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=SECRETROLE_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(SecretRole, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = SecretRole
         model = SecretRole

+ 2 - 15
netbox/tenancy/tables.py

@@ -1,6 +1,6 @@
 import django_tables2 as tables
 import django_tables2 as tables
 
 
-from utilities.tables import BaseTable, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn
 from .models import Tenant, TenantGroup
 from .models import Tenant, TenantGroup
 
 
 MPTT_LINK = """
 MPTT_LINK = """
@@ -13,15 +13,6 @@ MPTT_LINK = """
 </span>
 </span>
 """
 """
 
 
-TENANTGROUP_ACTIONS = """
-<a href="{% url 'tenancy:tenantgroup_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.tenancy.change_tenantgroup %}
-    <a href="{% url 'tenancy:tenantgroup_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 COL_TENANT = """
 COL_TENANT = """
 {% if record.tenant %}
 {% if record.tenant %}
     <a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
     <a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}" title="{{ record.tenant.description }}">{{ record.tenant }}</a>
@@ -44,11 +35,7 @@ class TenantGroupTable(BaseTable):
     tenant_count = tables.Column(
     tenant_count = tables.Column(
         verbose_name='Tenants'
         verbose_name='Tenants'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=TENANTGROUP_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(TenantGroup, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = TenantGroup
         model = TenantGroup

+ 43 - 0
netbox/utilities/tables.py

@@ -123,6 +123,49 @@ class BooleanColumn(tables.Column):
         return mark_safe(rendered)
         return mark_safe(rendered)
 
 
 
 
+class ButtonsColumn(tables.TemplateColumn):
+    """
+    Render edit, delete, and changelog buttons for an object.
+
+    :param model: Model class to use for calculating URL view names
+    :param prepend_content: Additional template content to render in the column (optional)
+    """
+    attrs = {'td': {'class': 'text-right text-nowrap noprint'}}
+    # Note that braces are escaped to allow for string formatting prior to template rendering
+    template_code = """
+    <a href="{{% url '{app_label}:{model_name}_changelog' {pk_field}=record.{pk_field} %}}" class="btn btn-default btn-xs" title="Change log">
+        <i class="fa fa-history"></i>
+    </a>
+    {{% if perms.{app_label}.change_{model_name} %}}
+        <a href="{{% url '{app_label}:{model_name}_edit' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-warning" title="Edit">
+            <i class="fa fa-pencil"></i>
+        </a>
+    {{% endif %}}
+    {{% if perms.{app_label}.delete_{model_name} %}}
+        <a href="{{% url '{app_label}:{model_name}_delete' {pk_field}=record.{pk_field} %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger" title="Delete">
+            <i class="fa fa-trash"></i>
+        </a>
+    {{% endif %}}
+    """
+
+    def __init__(self, model, *args, pk_field='pk', prepend_template=None, **kwargs):
+        if prepend_template:
+            prepend_template = prepend_template.replace('{', '{{')
+            prepend_template = prepend_template.replace('}', '}}')
+            self.template_code = prepend_template + self.template_code
+
+        template_code = self.template_code.format(
+            app_label=model._meta.app_label,
+            model_name=model._meta.model_name,
+            pk_field=pk_field
+        )
+
+        super().__init__(template_code=template_code, *args, **kwargs)
+
+    def header(self):
+        return ''
+
+
 class ColorColumn(tables.Column):
 class ColorColumn(tables.Column):
     """
     """
     Display a color (#RRGGBB).
     Display a color (#RRGGBB).

+ 3 - 29
netbox/virtualization/tables.py

@@ -2,27 +2,9 @@ import django_tables2 as tables
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
 
 
 from tenancy.tables import COL_TENANT
 from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, ColoredLabelColumn, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, ButtonsColumn, ColoredLabelColumn, TagColumn, ToggleColumn
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 
 
-CLUSTERTYPE_ACTIONS = """
-<a href="{% url 'virtualization:clustertype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.virtualization.change_clustertype %}
-    <a href="{% url 'virtualization:clustertype_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
-CLUSTERGROUP_ACTIONS = """
-<a href="{% url 'virtualization:clustergroup_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
-    <i class="fa fa-history"></i>
-</a>
-{% if perms.virtualization.change_clustergroup %}
-    <a href="{% url 'virtualization:clustergroup_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
-{% endif %}
-"""
-
 VIRTUALMACHINE_STATUS = """
 VIRTUALMACHINE_STATUS = """
 <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
 <span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
 """
 """
@@ -44,11 +26,7 @@ class ClusterTypeTable(BaseTable):
     cluster_count = tables.Column(
     cluster_count = tables.Column(
         verbose_name='Clusters'
         verbose_name='Clusters'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=CLUSTERTYPE_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(ClusterType, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = ClusterType
         model = ClusterType
@@ -66,11 +44,7 @@ class ClusterGroupTable(BaseTable):
     cluster_count = tables.Column(
     cluster_count = tables.Column(
         verbose_name='Clusters'
         verbose_name='Clusters'
     )
     )
-    actions = tables.TemplateColumn(
-        template_code=CLUSTERGROUP_ACTIONS,
-        attrs={'td': {'class': 'text-right noprint'}},
-        verbose_name=''
-    )
+    actions = ButtonsColumn(ClusterGroup, pk_field='slug')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = ClusterGroup
         model = ClusterGroup