Преглед изворни кода

Merge pull request #8070 from netbox-community/8069-generic-children-view

Closes #8069: Generic children view
Jeremy Stretch пре 4 година
родитељ
комит
001c7e4b18

+ 25 - 36
netbox/dcim/views.py

@@ -36,26 +36,15 @@ from .models import (
 )
 
 
-class DeviceComponentsView(generic.ObjectView):
+class DeviceComponentsView(generic.ObjectChildrenView):
     queryset = Device.objects.all()
-    model = None
-    table = None
 
-    def get_components(self, request, instance):
-        return self.model.objects.restrict(request.user, 'view').filter(device=instance)
+    def get_children(self, request, parent):
+        return self.child_model.objects.restrict(request.user, 'view').filter(device=parent)
 
     def get_extra_context(self, request, instance):
-        components = self.get_components(request, instance)
-        table = self.table(data=components, user=request.user)
-        change_perm = f'{self.model._meta.app_label}.change_{self.model._meta.model_name}'
-        delete_perm = f'{self.model._meta.app_label}.delete_{self.model._meta.model_name}'
-        if request.user.has_perm(change_perm) or request.user.has_perm(delete_perm):
-            table.columns.show('pk')
-        paginate_table(table, request)
-
         return {
-            'table': table,
-            'active_tab': f"{self.model._meta.verbose_name_plural.replace(' ', '-')}",
+            'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '-')}",
         }
 
 
@@ -63,8 +52,8 @@ class DeviceTypeComponentsView(DeviceComponentsView):
     queryset = DeviceType.objects.all()
     template_name = 'dcim/devicetype/component_templates.html'
 
-    def get_components(self, request, instance):
-        return self.model.objects.restrict(request.user, 'view').filter(device_type=instance)
+    def get_children(self, request, parent):
+        return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent)
 
 
 class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
@@ -806,42 +795,42 @@ class DeviceTypeView(generic.ObjectView):
 
 
 class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
-    model = ConsolePortTemplate
+    child_model = ConsolePortTemplate
     table = tables.ConsolePortTemplateTable
 
 
 class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
-    model = ConsoleServerPortTemplate
+    child_model = ConsoleServerPortTemplate
     table = tables.ConsoleServerPortTemplateTable
 
 
 class DeviceTypePowerPortsView(DeviceTypeComponentsView):
-    model = PowerPortTemplate
+    child_model = PowerPortTemplate
     table = tables.PowerPortTemplateTable
 
 
 class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
-    model = PowerOutletTemplate
+    child_model = PowerOutletTemplate
     table = tables.PowerOutletTemplateTable
 
 
 class DeviceTypeInterfacesView(DeviceTypeComponentsView):
-    model = InterfaceTemplate
+    child_model = InterfaceTemplate
     table = tables.InterfaceTemplateTable
 
 
 class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
-    model = FrontPortTemplate
+    child_model = FrontPortTemplate
     table = tables.FrontPortTemplateTable
 
 
 class DeviceTypeRearPortsView(DeviceTypeComponentsView):
-    model = RearPortTemplate
+    child_model = RearPortTemplate
     table = tables.RearPortTemplateTable
 
 
 class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
-    model = DeviceBayTemplate
+    child_model = DeviceBayTemplate
     table = tables.DeviceBayTemplateTable
 
 
@@ -1337,61 +1326,61 @@ class DeviceView(generic.ObjectView):
 
 
 class DeviceConsolePortsView(DeviceComponentsView):
-    model = ConsolePort
+    child_model = ConsolePort
     table = tables.DeviceConsolePortTable
     template_name = 'dcim/device/consoleports.html'
 
 
 class DeviceConsoleServerPortsView(DeviceComponentsView):
-    model = ConsoleServerPort
+    child_model = ConsoleServerPort
     table = tables.DeviceConsoleServerPortTable
     template_name = 'dcim/device/consoleserverports.html'
 
 
 class DevicePowerPortsView(DeviceComponentsView):
-    model = PowerPort
+    child_model = PowerPort
     table = tables.DevicePowerPortTable
     template_name = 'dcim/device/powerports.html'
 
 
 class DevicePowerOutletsView(DeviceComponentsView):
-    model = PowerOutlet
+    child_model = PowerOutlet
     table = tables.DevicePowerOutletTable
     template_name = 'dcim/device/poweroutlets.html'
 
 
 class DeviceInterfacesView(DeviceComponentsView):
-    model = Interface
+    child_model = Interface
     table = tables.DeviceInterfaceTable
     template_name = 'dcim/device/interfaces.html'
 
-    def get_components(self, request, instance):
-        return instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
+    def get_children(self, request, parent):
+        return parent.vc_interfaces().restrict(request.user, 'view').prefetch_related(
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
             Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user))
         )
 
 
 class DeviceFrontPortsView(DeviceComponentsView):
-    model = FrontPort
+    child_model = FrontPort
     table = tables.DeviceFrontPortTable
     template_name = 'dcim/device/frontports.html'
 
 
 class DeviceRearPortsView(DeviceComponentsView):
-    model = RearPort
+    child_model = RearPort
     table = tables.DeviceRearPortTable
     template_name = 'dcim/device/rearports.html'
 
 
 class DeviceDeviceBaysView(DeviceComponentsView):
-    model = DeviceBay
+    child_model = DeviceBay
     table = tables.DeviceDeviceBayTable
     template_name = 'dcim/device/devicebays.html'
 
 
 class DeviceInventoryView(DeviceComponentsView):
-    model = InventoryItem
+    child_model = InventoryItem
     table = tables.DeviceInventoryItemTable
     template_name = 'dcim/device/inventory.html'
 

+ 44 - 105
netbox/ipam/views.py

@@ -453,106 +453,62 @@ class PrefixView(generic.ObjectView):
         }
 
 
-class PrefixPrefixesView(generic.ObjectView):
+class PrefixPrefixesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
+    child_model = Prefix
+    table = tables.PrefixTable
     template_name = 'ipam/prefix/prefixes.html'
 
-    def get_extra_context(self, request, instance):
-        # Find all child prefixes contained in this prefix
-        prefix_list = instance.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
-            'site', 'vlan', 'role',
-        )
+    def get_children(self, request, parent):
+        child_prefixes = parent.get_child_prefixes().restrict(request.user, 'view')
 
-        # Return List of requested Prefixes
+        # Add available prefixes if requested
         show_available = bool(request.GET.get('show_available', 'true') == 'true')
         show_assigned = bool(request.GET.get('show_assigned', 'true') == 'true')
-        child_prefixes = add_requested_prefixes(instance.prefix, prefix_list, show_available, show_assigned)
-
-        table = tables.PrefixTable(child_prefixes, user=request.user, exclude=('utilization',))
-        if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
-            table.columns.show('pk')
-        paginate_table(table, request)
+        child_prefixes = add_requested_prefixes(parent.prefix, child_prefixes, show_available, show_assigned)
 
-        bulk_querystring = 'vrf_id={}&within={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'change': request.user.has_perm('ipam.change_prefix'),
-            'delete': request.user.has_perm('ipam.delete_prefix'),
-        }
+        return child_prefixes
 
+    def get_extra_context(self, request, instance):
         return {
-            'table': table,
-            'permissions': permissions,
-            'bulk_querystring': bulk_querystring,
+            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&within={instance.prefix}",
             'active_tab': 'prefixes',
             'first_available_prefix': instance.get_first_available_prefix(),
-            'show_available': show_available,
-            'show_assigned': show_assigned,
+            'show_available': bool(request.GET.get('show_available', 'true') == 'true'),
+            'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
         }
 
 
-class PrefixIPRangesView(generic.ObjectView):
+class PrefixIPRangesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
+    child_model = IPRange
+    table = tables.IPRangeTable
     template_name = 'ipam/prefix/ip_ranges.html'
 
-    def get_extra_context(self, request, instance):
-        # Find all IPRanges belonging to this Prefix
-        ip_ranges = instance.get_child_ranges().restrict(request.user, 'view').prefetch_related('vrf')
-
-        table = tables.IPRangeTable(ip_ranges, user=request.user)
-        if request.user.has_perm('ipam.change_iprange') or request.user.has_perm('ipam.delete_iprange'):
-            table.columns.show('pk')
-        paginate_table(table, request)
-
-        bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'change': request.user.has_perm('ipam.change_iprange'),
-            'delete': request.user.has_perm('ipam.delete_iprange'),
-        }
+    def get_children(self, request, parent):
+        return parent.get_child_ranges().restrict(request.user, 'view')
 
+    def get_extra_context(self, request, instance):
         return {
-            'table': table,
-            'permissions': permissions,
-            'bulk_querystring': bulk_querystring,
+            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
             'active_tab': 'ip-ranges',
         }
 
 
-class PrefixIPAddressesView(generic.ObjectView):
+class PrefixIPAddressesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
+    child_model = IPAddress
+    table = tables.IPAddressTable
     template_name = 'ipam/prefix/ip_addresses.html'
 
-    def get_extra_context(self, request, instance):
-        # Find all IPAddresses belonging to this Prefix
-        ipaddresses = instance.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf')
-
-        # Add available IP addresses to the table if requested
-        if request.GET.get('show_available', 'true') == 'true':
-            ipaddresses = add_available_ipaddresses(instance.prefix, ipaddresses, instance.is_pool)
-
-        table = tables.IPAddressTable(ipaddresses, user=request.user)
-        if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
-            table.columns.show('pk')
-        paginate_table(table, request)
-
-        bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'change': request.user.has_perm('ipam.change_ipaddress'),
-            'delete': request.user.has_perm('ipam.delete_ipaddress'),
-        }
+    def get_children(self, request, parent):
+        return parent.get_child_ips().restrict(request.user, 'view')
 
+    def get_extra_context(self, request, instance):
         return {
-            'table': table,
-            'permissions': permissions,
-            'bulk_querystring': bulk_querystring,
+            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
             'active_tab': 'ip-addresses',
             'first_available_ip': instance.get_first_available_ip(),
-            'show_available': request.GET.get('show_available', 'true') == 'true',
         }
 
 
@@ -600,35 +556,18 @@ class IPRangeView(generic.ObjectView):
     queryset = IPRange.objects.all()
 
 
-class IPRangeIPAddressesView(generic.ObjectView):
+class IPRangeIPAddressesView(generic.ObjectChildrenView):
     queryset = IPRange.objects.all()
+    child_model = IPAddress
+    table = tables.IPAddressTable
     template_name = 'ipam/iprange/ip_addresses.html'
 
-    def get_extra_context(self, request, instance):
-        # Find all IPAddresses within this range
-        ipaddresses = instance.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf')
-
-        # Add available IP addresses to the table if requested
-        # if request.GET.get('show_available', 'true') == 'true':
-        #     ipaddresses = add_available_ipaddresses(instance.prefix, ipaddresses, instance.is_pool)
-
-        ip_table = tables.IPAddressTable(ipaddresses)
-        if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
-            ip_table.columns.show('pk')
-        paginate_table(ip_table, request)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'add': request.user.has_perm('ipam.add_ipaddress'),
-            'change': request.user.has_perm('ipam.change_ipaddress'),
-            'delete': request.user.has_perm('ipam.delete_ipaddress'),
-        }
+    def get_children(self, request, parent):
+        return parent.get_child_ips().restrict(request.user, 'view')
 
+    def get_extra_context(self, request, instance):
         return {
-            'ip_table': ip_table,
-            'permissions': permissions,
             'active_tab': 'ip-addresses',
-            'show_available': request.GET.get('show_available', 'true') == 'true',
         }
 
 
@@ -1016,32 +955,32 @@ class VLANView(generic.ObjectView):
         }
 
 
-class VLANInterfacesView(generic.ObjectView):
+class VLANInterfacesView(generic.ObjectChildrenView):
     queryset = VLAN.objects.all()
+    child_model = Interface
+    table = tables.VLANDevicesTable
     template_name = 'ipam/vlan/interfaces.html'
 
-    def get_extra_context(self, request, instance):
-        interfaces = instance.get_interfaces().prefetch_related('device')
-        members_table = tables.VLANDevicesTable(interfaces)
-        paginate_table(members_table, request)
+    def get_children(self, request, parent):
+        return parent.get_interfaces().restrict(request.user, 'view')
 
+    def get_extra_context(self, request, instance):
         return {
-            'members_table': members_table,
             'active_tab': 'interfaces',
         }
 
 
-class VLANVMInterfacesView(generic.ObjectView):
+class VLANVMInterfacesView(generic.ObjectChildrenView):
     queryset = VLAN.objects.all()
+    child_model = VMInterface
+    table = tables.VLANVirtualMachinesTable
     template_name = 'ipam/vlan/vminterfaces.html'
 
-    def get_extra_context(self, request, instance):
-        interfaces = instance.get_vminterfaces().prefetch_related('virtual_machine')
-        members_table = tables.VLANVirtualMachinesTable(interfaces)
-        paginate_table(members_table, request)
+    def get_children(self, request, parent):
+        return parent.get_vminterfaces().restrict(request.user, 'view')
 
+    def get_extra_context(self, request, instance):
         return {
-            'members_table': members_table,
             'active_tab': 'vminterfaces',
         }
 

+ 48 - 0
netbox/netbox/views/generic.py

@@ -72,6 +72,54 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
         })
 
 
+class ObjectChildrenView(ObjectView):
+    """
+    Display a table of child objects associated with the parent object.
+
+    queryset: The base queryset for retrieving the *parent* object
+    table: Table class used to render child objects list
+    template_name: Name of the template to use
+    """
+    queryset = None
+    child_model = None
+    table = None
+    template_name = None
+
+    def get_children(self, request, parent):
+        """
+        Return a QuerySet or iterable of child objects.
+
+        request: The current request
+        parent: The parent object
+        """
+        raise NotImplementedError(f'{self.__class__.__name__} must implement get_children()')
+
+    def get(self, request, *args, **kwargs):
+        """
+        GET handler for rendering child objects.
+        """
+        instance = get_object_or_404(self.queryset, **kwargs)
+        child_objects = self.get_children(request, instance)
+
+        permissions = {}
+        for action in ('change', 'delete'):
+            perm_name = get_permission_for_model(self.child_model, action)
+            permissions[action] = request.user.has_perm(perm_name)
+
+        table = self.table(child_objects, user=request.user)
+        # Determine whether to display bulk action checkboxes
+        if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
+            table.columns.show('pk')
+        paginate_table(table, request)
+
+        return render(request, self.get_template_name(), {
+            'object': instance,
+            'table': table,
+            'permissions': permissions,
+            **self.get_extra_context(request, instance),
+        })
+
+
 class ObjectListView(ObjectPermissionRequiredMixin, View):
     """
     List a series of objects.

+ 1 - 1
netbox/templates/ipam/iprange/ip_addresses.html

@@ -11,7 +11,7 @@
 {% block content %}
   <div class="row">
     <div class="col col-md-12">
-      {% include 'utilities/obj_table.html' with table=ip_table heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
+      {% include 'utilities/obj_table.html' with heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
     </div>
   </div>
 {% endblock %}

+ 1 - 1
netbox/templates/ipam/vlan/interfaces.html

@@ -3,7 +3,7 @@
 {% block content %}
   <div class="row">
     <div class="col col-md-12">
-      {% include 'utilities/obj_table.html' with table=members_table heading='Device Interfaces' parent=vlan %}
+      {% include 'utilities/obj_table.html' with heading='Device Interfaces' parent=vlan %}
     </div>
   </div>
 {% endblock %}

+ 1 - 1
netbox/templates/ipam/vlan/vminterfaces.html

@@ -3,7 +3,7 @@
 {% block content %}
   <div class="row">
     <div class="col col-md-12">
-      {% include 'utilities/obj_table.html' with table=members_table heading='Virtual Machine Interfaces' parent=vlan %}
+      {% include 'utilities/obj_table.html' with heading='Virtual Machine Interfaces' parent=vlan %}
     </div>
   </div>
 {% endblock %}

+ 1 - 1
netbox/templates/virtualization/cluster/devices.html

@@ -12,7 +12,7 @@
       <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
       {% csrf_token %}
       <div class="card-body table-responsive">
-        {% render_table devices_table 'inc/table.html' %}
+        {% render_table table 'inc/table.html' %}
       </div>
       {% if perms.virtualization.change_cluster %}
         <div class="card-footer noprint">

+ 1 - 1
netbox/templates/virtualization/cluster/virtual_machines.html

@@ -10,7 +10,7 @@
         Virtual Machines
       </h5>
       <div class="card-body table-responsive">
-        {% render_table virtualmachines_table 'inc/table.html' %}
+        {% render_table table 'inc/table.html' %}
       </div>
     </div>
   </div>

+ 2 - 2
netbox/templates/virtualization/virtualmachine/interfaces.html

@@ -8,7 +8,7 @@
     {% csrf_token %}
     {% include 'inc/table_controls.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
     <div class="table-responsive">
-      {% render_table interface_table 'inc/table.html' %}
+      {% render_table table 'inc/table.html' %}
     </div>
     <div class="noprint">
         {% if perms.virtualization.change_vminterface %}
@@ -34,5 +34,5 @@
         <div class="clearfix"></div>
      </div>
   </form>
-  {% table_config_form interface_table %}
+  {% table_config_form table %}
 {% endblock %}

+ 18 - 30
netbox/virtualization/views.py

@@ -161,38 +161,32 @@ class ClusterView(generic.ObjectView):
     queryset = Cluster.objects.all()
 
 
-class ClusterVirtualMachinesView(generic.ObjectView):
+class ClusterVirtualMachinesView(generic.ObjectChildrenView):
     queryset = Cluster.objects.all()
+    child_model = VirtualMachine
+    table = tables.VirtualMachineTable
     template_name = 'virtualization/cluster/virtual_machines.html'
 
-    def get_extra_context(self, request, instance):
-        virtualmachines = VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=instance)
-        virtualmachines_table = tables.VirtualMachineTable(
-            virtualmachines,
-            exclude=('cluster',),
-            orderable=False
-        )
+    def get_children(self, request, parent):
+        return VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=parent)
 
+    def get_extra_context(self, request, instance):
         return {
-            'virtualmachines_table': virtualmachines_table,
             'active_tab': 'virtual-machines',
         }
 
 
-class ClusterDevicesView(generic.ObjectView):
+class ClusterDevicesView(generic.ObjectChildrenView):
     queryset = Cluster.objects.all()
+    child_model = Device
+    table = DeviceTable
     template_name = 'virtualization/cluster/devices.html'
 
-    def get_extra_context(self, request, instance):
-        devices = Device.objects.restrict(request.user, 'view').filter(cluster=instance).prefetch_related(
-            'site', 'rack', 'tenant', 'device_type__manufacturer'
-        )
-        devices_table = DeviceTable(list(devices), orderable=False)
-        if request.user.has_perm('virtualization.change_cluster'):
-            devices_table.columns.show('pk')
+    def get_children(self, request, parent):
+        return Device.objects.restrict(request.user, 'view').filter(cluster=parent)
 
+    def get_extra_context(self, request, instance):
         return {
-            'devices_table': devices_table,
             'active_tab': 'devices',
         }
 
@@ -347,26 +341,20 @@ class VirtualMachineView(generic.ObjectView):
         }
 
 
-class VirtualMachineInterfacesView(generic.ObjectView):
+class VirtualMachineInterfacesView(generic.ObjectChildrenView):
     queryset = VirtualMachine.objects.all()
+    child_model = VMInterface
+    table = tables.VMInterfaceTable
     template_name = 'virtualization/virtualmachine/interfaces.html'
 
-    def get_extra_context(self, request, instance):
-        interfaces = instance.interfaces.restrict(request.user, 'view').prefetch_related(
+    def get_children(self, request, parent):
+        return parent.interfaces.restrict(request.user, 'view').prefetch_related(
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
             'tags',
         )
-        interface_table = tables.VirtualMachineVMInterfaceTable(
-            data=interfaces,
-            user=request.user,
-            orderable=False
-        )
-        if request.user.has_perm('virtualization.change_vminterface') or \
-                request.user.has_perm('virtualization.delete_vminterface'):
-            interface_table.columns.show('pk')
 
+    def get_extra_context(self, request, instance):
         return {
-            'interface_table': interface_table,
             'active_tab': 'interfaces',
         }