Просмотр исходного кода

Merge branch 'develop' into feature

jeremystretch 4 лет назад
Родитель
Сommit
5e32c69e0e

+ 5 - 0
docs/release-notes/version-3.1.md

@@ -2,6 +2,11 @@
 
 ## v3.1.3 (FUTURE)
 
+### Bug Fixes
+
+* [#7962](https://github.com/netbox-community/netbox/issues/7962) - Fix user menu under report/script result view
+* [#8131](https://github.com/netbox-community/netbox/issues/8131) - Restore annotation of available IPs under prefix IPs view
+
 ---
 
 ## v3.1.2 (2021-12-20)

+ 0 - 43
netbox/dcim/models/__init__.py

@@ -5,46 +5,3 @@ from .devices import *
 from .power import *
 from .racks import *
 from .sites import *
-
-__all__ = (
-    'BaseInterface',
-    'Cable',
-    'CablePath',
-    'LinkTermination',
-    'ConsolePort',
-    'ConsolePortTemplate',
-    'ConsoleServerPort',
-    'ConsoleServerPortTemplate',
-    'Device',
-    'DeviceBay',
-    'DeviceBayTemplate',
-    'DeviceRole',
-    'DeviceType',
-    'FrontPort',
-    'FrontPortTemplate',
-    'Interface',
-    'InterfaceTemplate',
-    'InventoryItem',
-    'Location',
-    'Manufacturer',
-    'Module',
-    'ModuleBay',
-    'ModuleBayTemplate',
-    'ModuleType',
-    'Platform',
-    'PowerFeed',
-    'PowerOutlet',
-    'PowerOutletTemplate',
-    'PowerPanel',
-    'PowerPort',
-    'PowerPortTemplate',
-    'Rack',
-    'RackReservation',
-    'RackRole',
-    'RearPort',
-    'RearPortTemplate',
-    'Region',
-    'Site',
-    'SiteGroup',
-    'VirtualChassis',
-)

+ 9 - 18
netbox/dcim/tables/devicetypes.py

@@ -112,8 +112,7 @@ class ComponentTemplateTable(BaseTable):
 class ConsolePortTemplateTable(ComponentTemplateTable):
     actions = ButtonsColumn(
         model=ConsolePortTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_consoleports'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):
@@ -125,8 +124,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
 class ConsoleServerPortTemplateTable(ComponentTemplateTable):
     actions = ButtonsColumn(
         model=ConsoleServerPortTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_consoleserverports'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):
@@ -138,8 +136,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
 class PowerPortTemplateTable(ComponentTemplateTable):
     actions = ButtonsColumn(
         model=PowerPortTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_powerports'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):
@@ -151,8 +148,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
 class PowerOutletTemplateTable(ComponentTemplateTable):
     actions = ButtonsColumn(
         model=PowerOutletTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_poweroutlets'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):
@@ -167,8 +163,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
     )
     actions = ButtonsColumn(
         model=InterfaceTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_interfaces'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):
@@ -184,8 +179,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
     color = ColorColumn()
     actions = ButtonsColumn(
         model=FrontPortTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_frontports'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):
@@ -198,8 +192,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
     color = ColorColumn()
     actions = ButtonsColumn(
         model=RearPortTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_rearports'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):
@@ -211,8 +204,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
 class ModuleBayTemplateTable(ComponentTemplateTable):
     actions = ButtonsColumn(
         model=ModuleBayTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_modulebays'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):
@@ -224,8 +216,7 @@ class ModuleBayTemplateTable(ComponentTemplateTable):
 class DeviceBayTemplateTable(ComponentTemplateTable):
     actions = ButtonsColumn(
         model=DeviceBayTemplate,
-        buttons=('edit', 'delete'),
-        return_url_extra='%23tab_devicebays'
+        buttons=('edit', 'delete')
     )
 
     class Meta(ComponentTemplateTable.Meta):

+ 20 - 7
netbox/dcim/views.py

@@ -27,13 +27,7 @@ from virtualization.models import VirtualMachine
 from . import filtersets, forms, tables
 from .choices import DeviceFaceChoices
 from .constants import NONCONNECTABLE_IFACE_TYPES
-from .models import (
-    Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
-    DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
-    InventoryItem, Manufacturer, Module, ModuleBay, ModuleBayTemplate, ModuleType, PathEndpoint, Platform, PowerFeed,
-    PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate, Rack, Location, RackReservation,
-    RackRole, RearPort, RearPortTemplate, Region, Site, SiteGroup, VirtualChassis,
-)
+from .models import *
 
 
 class DeviceComponentsView(generic.ObjectChildrenView):
@@ -51,10 +45,21 @@ class DeviceComponentsView(generic.ObjectChildrenView):
 class DeviceTypeComponentsView(DeviceComponentsView):
     queryset = DeviceType.objects.all()
     template_name = 'dcim/devicetype/component_templates.html'
+    viewname = None  # Used for return_url resolution
 
     def get_children(self, request, parent):
         return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent)
 
+    def get_extra_context(self, request, instance):
+        if self.viewname:
+            return_url = reverse(self.viewname, kwargs={'pk': instance.pk})
+        else:
+            return_url = instance.get_absolute_url()
+        return {
+            'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '-')}",
+            'return_url': return_url,
+        }
+
 
 class ModuleTypeComponentsView(DeviceComponentsView):
     queryset = ModuleType.objects.all()
@@ -806,42 +811,49 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
     child_model = ConsolePortTemplate
     table = tables.ConsolePortTemplateTable
     filterset = filtersets.ConsolePortTemplateFilterSet
+    viewname = 'dcim:devicetype_consoleports'
 
 
 class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
     child_model = ConsoleServerPortTemplate
     table = tables.ConsoleServerPortTemplateTable
     filterset = filtersets.ConsoleServerPortTemplateFilterSet
+    viewname = 'dcim:devicetype_consoleserverports'
 
 
 class DeviceTypePowerPortsView(DeviceTypeComponentsView):
     child_model = PowerPortTemplate
     table = tables.PowerPortTemplateTable
     filterset = filtersets.PowerPortTemplateFilterSet
+    viewname = 'dcim:devicetype_powerports'
 
 
 class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
     child_model = PowerOutletTemplate
     table = tables.PowerOutletTemplateTable
     filterset = filtersets.PowerOutletTemplateFilterSet
+    viewname = 'dcim:devicetype_poweroutlets'
 
 
 class DeviceTypeInterfacesView(DeviceTypeComponentsView):
     child_model = InterfaceTemplate
     table = tables.InterfaceTemplateTable
     filterset = filtersets.InterfaceTemplateFilterSet
+    viewname = 'dcim:devicetype_interfaces'
 
 
 class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
     child_model = FrontPortTemplate
     table = tables.FrontPortTemplateTable
     filterset = filtersets.FrontPortTemplateFilterSet
+    viewname = 'dcim:devicetype_frontports'
 
 
 class DeviceTypeRearPortsView(DeviceTypeComponentsView):
     child_model = RearPortTemplate
     table = tables.RearPortTemplateTable
     filterset = filtersets.RearPortTemplateFilterSet
+    viewname = 'dcim:devicetype_rearports'
 
 
 class DeviceTypeModuleBaysView(DeviceTypeComponentsView):
@@ -854,6 +866,7 @@ class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
     child_model = DeviceBayTemplate
     table = tables.DeviceBayTemplateTable
     filterset = filtersets.DeviceBayTemplateFilterSet
+    viewname = 'dcim:devicetype_devicebays'
 
 
 class DeviceTypeEditView(generic.ObjectEditView):

+ 3 - 3
netbox/extras/api/serializers.py

@@ -5,10 +5,10 @@ from drf_yasg.utils import swagger_serializer_method
 from rest_framework import serializers
 
 from dcim.api.nested_serializers import (
-    NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer,
-    NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
+    NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedRegionSerializer,
+    NestedSiteSerializer, NestedSiteGroupSerializer,
 )
-from dcim.models import Device, DeviceRole, DeviceType, Platform, Rack, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
 from extras.choices import *
 from extras.models import *
 from extras.utils import FeatureQuery

+ 25 - 4
netbox/extras/views.py

@@ -10,6 +10,7 @@ from rq import Worker
 
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
+from utilities.htmx import is_htmx
 from utilities.tables import paginate_table
 from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict
 from utilities.views import ContentTypePermissionRequiredMixin
@@ -693,16 +694,26 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View):
 
     def get(self, request, job_result_pk):
         report_content_type = ContentType.objects.get(app_label='extras', model='report')
-        jobresult = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
+        result = get_object_or_404(JobResult.objects.all(), pk=job_result_pk, obj_type=report_content_type)
 
         # Retrieve the Report and attach the JobResult to it
-        module, report_name = jobresult.name.split('.')
+        module, report_name = result.name.split('.')
         report = get_report(module, report_name)
-        report.result = jobresult
+        report.result = result
+
+        # If this is an HTMX request, return only the result HTML
+        if is_htmx(request):
+            response = render(request, 'extras/htmx/report_result.html', {
+                'report': report,
+                'result': result,
+            })
+            if result.completed:
+                response.status_code = 286
+            return response
 
         return render(request, 'extras/report_result.html', {
             'report': report,
-            'result': jobresult,
+            'result': result,
         })
 
 
@@ -820,6 +831,16 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View)
 
         script = self._get_script(result.name)
 
+        # If this is an HTMX request, return only the result HTML
+        if is_htmx(request):
+            response = render(request, 'extras/htmx/script_result.html', {
+                'script': script,
+                'result': result,
+            })
+            if result.completed:
+                response.status_code = 286
+            return response
+
         return render(request, 'extras/script_result.html', {
             'script': script,
             'result': result,

+ 10 - 3
netbox/ipam/views.py

@@ -5,18 +5,18 @@ from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 
 from dcim.filtersets import InterfaceFilterSet
-from dcim.models import Device, Interface, Site
+from dcim.models import Interface, Site
 from dcim.tables import SiteTable
 from netbox.views import generic
 from utilities.tables import paginate_table
 from utilities.utils import count_related
 from virtualization.filtersets import VMInterfaceFilterSet
-from virtualization.models import VirtualMachine, VMInterface
+from virtualization.models import VMInterface
 from . import filtersets, forms, tables
 from .constants import *
 from .models import *
 from .models import ASN
-from .utils import add_requested_prefixes, add_available_vlans
+from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans
 
 
 #
@@ -502,6 +502,13 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
     def get_children(self, request, parent):
         return parent.get_child_ips().restrict(request.user, 'view')
 
+    def prep_table_data(self, request, queryset, parent):
+        show_available = bool(request.GET.get('show_available', 'true') == 'true')
+        if show_available:
+            return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool)
+
+        return queryset
+
     def get_extra_context(self, request, instance):
         return {
             'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",

+ 0 - 1
netbox/project-static/bundle.js

@@ -40,7 +40,6 @@ async function bundleGraphIQL() {
 async function bundleNetBox() {
   const entryPoints = {
     netbox: 'src/index.ts',
-    jobs: 'src/jobs.ts',
     lldp: 'src/device/lldp.ts',
     config: 'src/device/config.ts',
     status: 'src/device/status.ts',

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/jobs.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 2
netbox/project-static/dist/jobs.js.map


+ 0 - 32
netbox/project-static/src/global.d.ts

@@ -98,38 +98,6 @@ type APISecret = {
   url: string;
 };
 
-type JobResultLog = {
-  message: string;
-  status: 'success' | 'warning' | 'danger' | 'info';
-};
-
-type JobStatus = {
-  label: string;
-  value: 'completed' | 'failed' | 'errored' | 'running';
-};
-
-type APIJobResult = {
-  completed: string;
-  created: string;
-  data: {
-    log: JobResultLog[];
-    output: string;
-  };
-  display: string;
-  id: number;
-  job_id: string;
-  name: string;
-  obj_type: string;
-  status: JobStatus;
-  url: string;
-  user: {
-    display: string;
-    username: string;
-    id: number;
-    url: string;
-  };
-};
-
 type APIUserConfig = {
   tables: { [k: string]: { columns: string[]; available_columns: string[] } };
   [k: string]: unknown;

+ 0 - 104
netbox/project-static/src/jobs.ts

@@ -1,104 +0,0 @@
-import { createToast } from './bs';
-import { apiGetBase, hasError, getNetboxData } from './util';
-
-let timeout: number = 1000;
-
-interface JobInfo {
-  url: Nullable<string>;
-  complete: boolean;
-}
-
-/**
- * Mimic the behavior of setTimeout() in an async function.
- */
-function asyncTimeout(ms: number) {
-  return new Promise(resolve => setTimeout(resolve, ms));
-}
-
-/**
- * Job ID & Completion state are only from Django context, which can only be used from the HTML
- * template. Hidden elements are present in the template to provide access to these values from
- * JavaScript.
- */
-function getJobInfo(): JobInfo {
-  let complete = false;
-
-  // Determine the API URL for the job status
-  const url = getNetboxData('data-job-url');
-
-  // Determine the job completion status, if present. If the job is not complete, the value will be
-  // "None". Otherwise, it will be a stringified date.
-  const jobComplete = getNetboxData('data-job-complete');
-  if (typeof jobComplete === 'string' && jobComplete.toLowerCase() !== 'none') {
-    complete = true;
-  }
-  return { url, complete };
-}
-
-/**
- * Update the job status label element based on the API response.
- */
-function updateLabel(status: JobStatus) {
-  const element = document.querySelector<HTMLSpanElement>('#pending-result-label > span.badge');
-  if (element !== null) {
-    let labelClass = 'secondary';
-    switch (status.value) {
-      case 'failed' || 'errored':
-        labelClass = 'danger';
-        break;
-      case 'running':
-        labelClass = 'warning';
-        break;
-      case 'completed':
-        labelClass = 'success';
-        break;
-    }
-    element.setAttribute('class', `badge bg-${labelClass}`);
-    element.innerText = status.label;
-  }
-}
-
-/**
- * Recursively check the job's status.
- * @param url API URL for job result
- */
-async function checkJobStatus(url: string) {
-  const res = await apiGetBase<APIJobResult>(url);
-  if (hasError(res)) {
-    // If the response is an API error, display an error message and stop checking for job status.
-    const toast = createToast('danger', 'Error', res.error);
-    toast.show();
-    return;
-  } else {
-    // Update the job status label.
-    updateLabel(res.status);
-
-    // If the job is complete, reload the page.
-    if (['completed', 'failed', 'errored'].includes(res.status.value)) {
-      location.reload();
-      return;
-    } else {
-      // Otherwise, keep checking the job's status, backing off 1 second each time, until a 10
-      // second interval is reached.
-      if (timeout < 10000) {
-        timeout += 1000;
-      }
-      await Promise.all([checkJobStatus(url), asyncTimeout(timeout)]);
-    }
-  }
-}
-
-function initJobs() {
-  const { url, complete } = getJobInfo();
-
-  if (url !== null && !complete) {
-    // If there is a job ID and it is not completed, check for the job's status.
-    Promise.resolve(checkJobStatus(url));
-  }
-}
-
-if (document.readyState !== 'loading') {
-  initJobs();
-} else {
-  document.addEventListener('DOMContentLoaded', initJobs);
-}

+ 31 - 30
netbox/templates/dcim/device/base.html

@@ -102,82 +102,83 @@
         </a>
     </li>
 
-    {% with devicebay_count=object.devicebays.count %}
-        {% if devicebay_count %}
+    {% with tab_name='device-bays' devicebay_count=object.devicebays.count %}
+        {% if active_tab == tab_name or devicebay_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'device-bays' %} active{% endif %}" href="{% url 'dcim:device_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with modulebay_count=object.modulebays.count %}
-        {% if modulebay_count %}
+    {% with tab_name='module-bays' modulebay_count=object.modulebays.count %}
+        {% if active_tab == tab_name or modulebay_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'module-bays' %} active{% endif %}" href="{% url 'dcim:device_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with interface_count=object.interfaces_count %}
-        {% if interface_count %}
+    {% with tab_name='interfaces' interface_count=object.interfaces_count %}
+        {% if active_tab == tab_name or interface_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'interfaces' %} active{% endif %}" href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with frontport_count=object.frontports.count %}
-        {% if frontport_count %}
+    {% with tab_name='front-ports' frontport_count=object.frontports.count %}
+        {% if active_tab == tab_name or frontport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'front-ports' %} active{% endif %}" href="{% url 'dcim:device_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with rearport_count=object.rearports.count %}
-        {% if rearport_count %}
+    {% with tab_name='rear-ports' rearport_count=object.rearports.count %}
+        {% if active_tab == tab_name or rearport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'rear-ports' %} active{% endif %}" href="{% url 'dcim:device_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with consoleport_count=object.consoleports.count %}
-        {% if consoleport_count %}
+    {% with tab_name='console-ports' consoleport_count=object.consoleports.count %}
+        {% if active_tab == tab_name or consoleport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'console-ports' %} active{% endif %}" href="{% url 'dcim:device_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with consoleserverport_count=object.consoleserverports.count %}
-        {% if consoleserverport_count %}
+    {% with tab_name='console-server-ports' consoleserverport_count=object.consoleserverports.count %}
+        {% if active_tab == tab_name or consoleserverport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'console-server-ports' %} active{% endif %}" href="{% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with powerport_count=object.powerports.count %}
-        {% if powerport_count %}
+    {% with tab_name='power-ports' powerport_count=object.powerports.count %}
+        {% if active_tab == tab_name or powerport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'power-ports' %} active{% endif %}" href="{% url 'dcim:device_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with poweroutlet_count=object.poweroutlets.count %}
-        {% if poweroutlet_count %}
+    {% with tab_name='power-outlets' poweroutlet_count=object.poweroutlets.count %}
+        {% if active_tab == tab_name or poweroutlet_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'power-outlets' %} active{% endif %}" href="{% url 'dcim:device_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with inventoryitem_count=object.inventoryitems.count %}
-        {% if inventoryitem_count %}
+
+    {% with tab_name='inventory-items' inventoryitem_count=object.inventoryitems.count %}
+        {% if active_tab == tab_name or inventoryitem_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'inventory' %} active{% endif %}" href="{% url 'dcim:device_inventory' pk=object.pk %}">Inventory {% badge inventoryitem_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:device_inventory' pk=object.pk %}">Inventory {% badge inventoryitem_count %}</a>
             </li>
         {% endif %}
     {% endwith %}

+ 4 - 4
netbox/templates/dcim/device/devicebays.html

@@ -17,22 +17,22 @@
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_devicebay %}
-                <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-outline-warning btn-sm">
+                <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
-                <button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-warning btn-sm">
+                <button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-warning btn-sm">
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                 </button>
             {% endif %}
             {% if perms.dcim.delete_devicebay %}
-                <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-outline-danger btn-sm">
+                <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
                 </button>
             {% endif %}
         </div>
         {% if perms.dcim.add_devicebay %}
             <div class="bulk-button-group">
-                <a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-primary btn-sm">
+                <a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
                 </a>
             </div>

+ 4 - 4
netbox/templates/dcim/device/modulebays.html

@@ -17,22 +17,22 @@
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_modulebay %}
-                <button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={{ object.get_absolute_url }}%23tab_modulebays" class="btn btn-outline-warning btn-sm">
+                <button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                     <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
                 </button>
-                <button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_modulebays" class="btn btn-warning btn-sm">
+                <button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-warning btn-sm">
                     <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
                 </button>
             {% endif %}
             {% if perms.dcim.delete_modulebay %}
-                <button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={{ object.get_absolute_url }}%23tab_modulebays" class="btn btn-outline-danger btn-sm">
+                <button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
                     <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
                 </button>
             {% endif %}
         </div>
         {% if perms.dcim.add_modulebay %}
             <div class="bulk-button-group">
-                <a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_modulebays" class="btn btn-primary btn-sm">
+                <a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays
                 </a>
             </div>

+ 36 - 36
netbox/templates/dcim/devicetype/base.html

@@ -18,31 +18,31 @@
       </button>
       <ul class="dropdown-menu">
         {% if perms.dcim.add_consoleporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_consoleports' pk=object.pk %}">Console Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_consoleserverporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_consoleserverports' pk=object.pk %}">Console Server Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_powerporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_powerports' pk=object.pk %}">Power Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_poweroutlettemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_poweroutlets' pk=object.pk %}">Power Outlets</a></li>
         {% endif %}
         {% if perms.dcim.add_interfacetemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_interfaces' pk=object.pk %}">Interfaces</a></li>
         {% endif %}
         {% if perms.dcim.add_frontporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_frontports' pk=object.pk %}">Front Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_rearporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_rearports' pk=object.pk %}">Rear Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_modulebaytemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:modulebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_modulebays">Module Bays</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:modulebaytemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_modulebays' pk=object.pk %}">Module Bays</a></li>
         {% endif %}
         {% if perms.dcim.add_devicebaytemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays">Device Bays</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays</a></li>
         {% endif %}
       </ul>
     </div>
@@ -56,74 +56,74 @@
         </a>
     </li>
 
-    {% with devicebay_count=object.devicebaytemplates.count %}
-        {% if devicebay_count %}
+    {% with tab_name='device-bay-templates' devicebay_count=object.devicebaytemplates.count %}
+        {% if active_tab == tab_name or devicebay_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'device-bay-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with modulebay_count=object.modulebaytemplates.count %}
-        {% if modulebay_count %}
+    {% with tab_name='module-bay-templates' modulebay_count=object.modulebaytemplates.count %}
+        {% if active_tab == tab_name or modulebay_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'module-bay-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_modulebays' pk=object.pk %}">Module Bays {% badge modulebay_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with interface_count=object.interfacetemplates.count %}
-        {% if interface_count %}
+    {% with tab_name='interface-templates' interface_count=object.interfacetemplates.count %}
+        {% if active_tab == tab_name or interface_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'interface-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with frontport_count=object.frontporttemplates.count %}
-        {% if frontport_count %}
+    {% with tab_name='front-port-templates' frontport_count=object.frontporttemplates.count %}
+        {% if active_tab == tab_name or frontport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'front-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with rearport_count=object.rearporttemplates.count %}
-        {% if rearport_count %}
+    {% with tab_name='rear-port-templates' rearport_count=object.rearporttemplates.count %}
+        {% if active_tab == tab_name or rearport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'rear-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with consoleport_count=object.consoleporttemplates.count %}
-        {% if consoleport_count %}
+    {% with tab_name='console-port-templates' consoleport_count=object.consoleporttemplates.count %}
+        {% if active_tab == tab_name or consoleport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'console-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with consoleserverport_count=object.consoleserverporttemplates.count %}
-        {% if consoleserverport_count %}
+    {% with tab_name='console-server-port-templates' consoleserverport_count=object.consoleserverporttemplates.count %}
+        {% if active_tab == tab_name or consoleserverport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'console-server-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with powerport_count=object.powerporttemplates.count %}
-        {% if powerport_count %}
+    {% with tab_name='power-port-templates' powerport_count=object.powerporttemplates.count %}
+        {% if active_tab == tab_name or powerport_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'power-port-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
             </li>
         {% endif %}
     {% endwith %}
 
-    {% with poweroutlet_count=object.poweroutlettemplates.count %}
-        {% if poweroutlet_count %}
+    {% with tab_name='power-outlet-templates' poweroutlet_count=object.poweroutlettemplates.count %}
+        {% if active_tab == tab_name or poweroutlet_count %}
             <li role="presentation" class="nav-item">
-                <a class="nav-link {% if active_tab == 'power-outlet-templates' %} active{% endif %}" href="{% url 'dcim:devicetype_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
+                <a class="nav-link {% if active_tab == tab_name %} active{% endif %}" href="{% url 'dcim:devicetype_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
             </li>
         {% endif %}
     {% endwith %}

+ 4 - 4
netbox/templates/dcim/devicetype/component_templates.html

@@ -13,18 +13,18 @@
             </div>
             <div class="card-footer noprint">
                 {% if table.rows %}
-                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning">
+                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ return_url }}" class="btn btn-sm btn-warning">
                         <span class="mdi mdi-pencil-outline" aria-hidden="true"></span> Rename
                     </button>
-                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning">
+                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ return_url }}" class="btn btn-sm btn-warning">
                         <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
                     </button>
-                    <button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger">
+                    <button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ return_url }}" class="btn btn-sm btn-danger">
                         <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     </button>
                 {% endif %}
                 <div class="float-end">
-                    <a href="{% url table.Meta.model|viewname:"add" %}?device_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_{{ tab }}" class="btn btn-primary btn-sm">
+                    <a href="{% url table.Meta.model|viewname:"add" %}?device_type={{ object.pk }}&return_url={{ return_url }}" class="btn btn-primary btn-sm">
                         <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
                         Add {{ title }}
                     </a>

+ 7 - 7
netbox/templates/dcim/moduletype/base.html

@@ -18,25 +18,25 @@
       </button>
       <ul class="dropdown-menu">
         {% if perms.dcim.add_consoleporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleports">Console Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_consoleports' pk=object.pk %}">Console Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_consoleserverporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_consoleserverports">Console Server Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_consoleserverports' pk=object.pk %}">Console Server Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_powerporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_powerports">Power Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_powerports' pk=object.pk %}">Power Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_poweroutlettemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_poweroutlets">Power Outlets</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_poweroutlets' pk=object.pk %}">Power Outlets</a></li>
         {% endif %}
         {% if perms.dcim.add_interfacetemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_interfaces">Interfaces</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_interfaces' pk=object.pk %}">Interfaces</a></li>
         {% endif %}
         {% if perms.dcim.add_frontporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_frontports">Front Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_frontports' pk=object.pk %}">Front Ports</a></li>
         {% endif %}
         {% if perms.dcim.add_rearporttemplate %}
-          <li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_rearports">Rear Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_rearports' pk=object.pk %}">Rear Ports</a></li>
         {% endif %}
       </ul>
     </div>

+ 4 - 4
netbox/templates/dcim/moduletype/component_templates.html

@@ -13,18 +13,18 @@
             </div>
             <div class="card-footer noprint">
                 {% if table.rows %}
-                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning">
+                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ return_url }}" class="btn btn-sm btn-warning">
                         <span class="mdi mdi-pencil-outline" aria-hidden="true"></span> Rename
                     </button>
-                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning">
+                    <button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ return_url }}" class="btn btn-sm btn-warning">
                         <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
                     </button>
-                    <button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger">
+                    <button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ return_url }}" class="btn btn-sm btn-danger">
                         <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                     </button>
                 {% endif %}
                 <div class="float-end">
-                    <a href="{% url table.Meta.model|viewname:"add" %}?module_type={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_{{ tab }}" class="btn btn-primary btn-sm">
+                    <a href="{% url table.Meta.model|viewname:"add" %}?module_type={{ object.pk }}&return_url={{ return_url }}" class="btn btn-primary btn-sm">
                         <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
                         Add {{ title }}
                     </a>

+ 73 - 0
netbox/templates/extras/htmx/report_result.html

@@ -0,0 +1,73 @@
+{% load helpers %}
+
+<p>
+  Initiated: <strong>{{ result.created|annotated_date }}</strong>
+  {% if result.completed %}
+    Duration: <strong>{{ result.duration }}</strong>
+  {% endif %}
+  <span id="pending-result-label">{% include 'extras/inc/job_label.html' %}</span>
+</p>
+{% if result.completed %}
+  <div class="card">
+    <h5 class="card-header">Report Methods</h5>
+    <div class="card-body">
+      <table class="table table-hover">
+        {% for method, data in result.data.items %}
+          <tr>
+            <td class="font-monospace"><a href="#{{ method }}">{{ method }}</a></td>
+            <td class="text-end report-stats">
+              <span class="badge bg-success">{{ data.success }}</span>
+              <span class="badge bg-info">{{ data.info }}</span>
+              <span class="badge bg-warning">{{ data.warning }}</span>
+              <span class="badge bg-danger">{{ data.failure }}</span>
+            </td>
+          </tr>
+        {% endfor %}
+      </table>
+    </div>
+  </div>
+  <div class="card">
+    <h5 class="card-header">Report Results</h5>
+    <div class="card-body">
+      <table class="table table-hover report">
+        <thead>
+          <tr class="table-headings">
+            <th>Time</th>
+            <th>Level</th>
+            <th>Object</th>
+            <th>Message</th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for method, data in result.data.items %}
+            <tr>
+              <th colspan="4" style="font-family: monospace">
+                <a name="{{ method }}"></a>{{ method }}
+              </th>
+            </tr>
+            {% for time, level, obj, url, message in data.log %}
+              <tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
+                <td>{{ time }}</td>
+                <td>
+                  <label class="badge bg-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
+                </td>
+                <td>
+                  {% if obj and url %}
+                    <a href="{{ url }}">{{ obj }}</a>
+                  {% elif obj %}
+                    {{ obj }}
+                  {% else %}
+                    <span class="muted">&mdash;</span>
+                  {% endif %}
+                </td>
+                <td class="rendered-markdown">{{ message|render_markdown }}</td>
+              </tr>
+            {% endfor %}
+          {% endfor %}
+        </tbody>
+      </table>
+    </div>
+  </div>
+{% else %}
+  {% include 'extras/inc/result_pending.html' %}
+{% endif %}

+ 50 - 0
netbox/templates/extras/htmx/script_result.html

@@ -0,0 +1,50 @@
+{% load helpers %}
+{% load log_levels %}
+
+<p>
+  Initiated: <strong>{{ result.created|annotated_date }}</strong>
+  {% if result.completed %}
+    Duration: <strong>{{ result.duration }}</strong>
+  {% endif %}
+  <span id="pending-result-label">{% include 'extras/inc/job_label.html' %}</span>
+</p>
+{% if result.completed %}
+  <div class="card mb-3">
+    <h5 class="card-header">Script Log</h5>
+    <div class="card-body">
+      <table class="table table-hover panel-body">
+        <tr>
+          <th>Line</th>
+          <th>Level</th>
+          <th>Message</th>
+        </tr>
+        {% for log in result.data.log %}
+          <tr>
+            <td>{{ forloop.counter }}</td>
+            <td>{% log_level log.status %}</td>
+            <td class="rendered-markdown">{{ log.message|render_markdown }}</td>
+          </tr>
+        {% empty %}
+          <tr>
+            <td colspan="3" class="text-center text-muted">
+              No log output
+            </td>
+          </tr>
+        {% endfor %}
+      </table>
+    </div>
+    {% if execution_time %}
+      <div class="card-footer text-end text-muted">
+        <small>Exec Time: {{ execution_time|floatformat:3 }}s</small>
+      </div>
+    {% endif %}
+  </div>
+  <h4>Output</h4>
+  {% if result.data.output %}
+    <pre class="block">{{ result.data.output }}</pre>
+  {% else %}
+    <p class="text-muted">None</p>
+  {% endif %}
+{% else %}
+  {% include 'extras/inc/result_pending.html' %}
+{% endif %}

+ 6 - 0
netbox/templates/extras/inc/result_pending.html

@@ -0,0 +1,6 @@
+{# Indicates that a job result is still pending; used for HTMX requests #}
+<div class="spinner-border float-start me-1" id="spinner" role="status">
+  <span class="visually-hidden">Loading...</span>
+</div>
+<h3>Results pending...</h3>
+<small class="text-muted">Last updated {% now "H:i:s" %}</small>

+ 4 - 94
netbox/templates/extras/report_result.html

@@ -1,99 +1,9 @@
 {% extends 'extras/report.html' %}
-{% load helpers %}
-{% load static %}
-
-{% block head %}
-<script src="{% static 'jobs.js' %}?v{{ settings.VERSION }}"
-        onerror="window.location='{% url 'media_failure' %}?filename=jobs.js'"></script>
-{% endblock %}
 
 {% block content-wrapper %}
-<div class="row px-3">
-    <div class="col col-md-12">
-        <p>
-            Run: <strong>{{ result.created|annotated_date }}</strong>
-            {% if result.completed %}
-                Duration: <strong>{{ result.duration }}</strong>
-            {% else %}
-                <div class="spinner-border" role="status">
-                    <span class="visually-hidden">Loading...</span>
-                </div>
-            {% endif %}
-            <span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
-        </p>
-        {% if result.completed %}
-            <div class="card">
-                <h5 class="card-header">
-                    Report Methods
-                </h5>
-                <div class="card-body">
-                    <table class="table table-hover">
-                        {% for method, data in result.data.items %}
-                            <tr>
-                                <td class="font-monospace"><a href="#{{ method }}">{{ method }}</a></td>
-                                <td class="text-end report-stats">
-                                    <span class="badge bg-success">{{ data.success }}</span>
-                                    <span class="badge bg-info">{{ data.info }}</span>
-                                    <span class="badge bg-warning">{{ data.warning }}</span>
-                                    <span class="badge bg-danger">{{ data.failure }}</span>
-                                </td>
-                            </tr>
-                        {% endfor %}
-                    </table>
-                </div>
-            </div>
-            <div class="card">
-                <h5 class="card-header">
-                    Report Results
-                </h5>
-                <div class="card-body">
-                    <table class="table table-hover report">
-                        <thead>
-                            <tr class="table-headings">
-                                <th>Time</th>
-                                <th>Level</th>
-                                <th>Object</th>
-                                <th>Message</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            {% for method, data in result.data.items %}
-                                <tr>
-                                    <th colspan="4" style="font-family: monospace">
-                                        <a name="{{ method }}"></a>{{ method }}
-                                    </th>
-                                </tr>
-                                {% for time, level, obj, url, message in data.log %}
-                                    <tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
-                                        <td>{{ time }}</td>
-                                        <td>
-                                            <label class="badge bg-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
-                                        </td>
-                                        <td>
-                                            {% if obj and url %}
-                                                <a href="{{ url }}">{{ obj }}</a>
-                                            {% elif obj %}
-                                                {{ obj }}
-                                            {% else %}
-                                                <span class="muted">&mdash;</span>
-                                            {% endif %}
-                                        </td>
-                                        <td class="rendered-markdown">{{ message|render_markdown }}</td>
-                                    </tr>
-                                {% endfor %}
-                            {% endfor %}
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-        {% else %}
-            <div class="well">Pending results</div>
-        {% endif %}
+  <div class="row px-3">
+    <div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:report_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
+      {% include 'extras/htmx/report_result.html' %}
     </div>
-</div>
-{% endblock %}
-
-{% block data %}
-<span data-job-url="{% url 'extras-api:jobresult-detail' pk=result.pk %}"></span>
-<span data-job-complete="{{ result.completed }}"></span>
+  </div>
 {% endblock %}

+ 26 - 95
netbox/templates/extras/script_result.html

@@ -1,117 +1,48 @@
 {% extends 'base/layout.html' %}
 {% load helpers %}
-{% load form_helpers %}
-{% load log_levels %}
-{% load static %}
-
-{% block head %}
-<script src="{% static 'jobs.js' %}?v{{ settings.VERSION }}"
-        onerror="window.location='{% url 'media_failure' %}?filename=jobs.js'"></script>
-{% endblock %}
 
 {% block title %}{{ script }}{% endblock %}
 
 {% block subtitle %}
   {{ script.Meta.description|render_markdown }}
-  <span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
 {% endblock %}
 
 {% block header %}
   <div class="row noprint">
-      <div class="col col-md-12">
-          <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
-              <ol class="breadcrumb">
-                  <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
-                  <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
-                  <li class="breadcrumb-item"><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
-                  <li class="breadcrumb-item">{{ result.created|annotated_date }}</li>
-              </ol>
-          </nav>
-      </div>
+    <div class="col col-md-12">
+      <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
+        <ol class="breadcrumb">
+          <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
+          <li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
+          <li class="breadcrumb-item"><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
+          <li class="breadcrumb-item">{{ result.created|annotated_date }}</li>
+        </ol>
+      </nav>
+    </div>
   </div>
   {{ block.super }}
 {% endblock header %}
 
 {% block content-wrapper %}
   <ul class="nav nav-tabs px-3" role="tablist">
-      <li class="nav-item" role="presentation">
-          <a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">Log</a>
-      </li>
-      <li class="nav-item" role="presentation">
-          <a href="#output" role="tab" data-bs-toggle="tab" class="nav-link">Output</a>
-      </li>
-      <li class="nav-item" role="presentation">
-          <a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
-      </li>
+    <li class="nav-item" role="presentation">
+      <a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">Log</a>
+    </li>
+    <li class="nav-item" role="presentation">
+      <a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
+    </li>
   </ul>
   <div class="tab-content mb-3">
-      <p>
-          Run: <strong>{{ result.created|annotated_date }}</strong>
-          {% if result.completed %}
-              Duration: <strong>{{ result.duration }}</strong>
-          {% else %}
-              <div class="spinner-border" role="status">
-                  <span class="visually-hidden">Loading...</span>
-              </div>
-          {% endif %}
-      </p>
-      <div role="tabpanel" class="tab-pane active" id="log">
-          {% if result.completed %}
-              <div class="row">
-                  <div class="col col-md-12">
-                      <div class="card">
-                          <h5 class="card-header">
-                              Script Log
-                          </h5>
-                          <div class="card-body">
-                              <table class="table table-hover panel-body">
-                                  <tr>
-                                      <th>Line</th>
-                                      <th>Level</th>
-                                      <th>Message</th>
-                                  </tr>
-                                  {% for log in result.data.log %}
-                                      <tr>
-                                          <td>{{ forloop.counter }}</td>
-                                          <td>{% log_level log.status %}</td>
-                                          <td class="rendered-markdown">{{ log.message|render_markdown }}</td>
-                                      </tr>
-                                  {% empty %}
-                                      <tr>
-                                          <td colspan="3" class="text-center text-muted">
-                                              No log output
-                                          </td>
-                                      </tr>
-                                  {% endfor %}
-                              </table>
-                          </div>
-                          {% if execution_time %}
-                              <div class="card-footer text-end text-muted">
-                                  <small>Exec Time: {{ execution_time|floatformat:3 }}s</small>
-                              </div>
-                          {% endif %}
-                      </div>
-                  </div>
-              </div>
-          {% else %}
-              <div class="row">
-                  <div class="col col-md-12">
-                      <div class="well">Pending Results</div>
-                  </div>
-              </div>
-          {% endif %}
-      </div>
-      <div role="tabpanel" class="tab-pane" id="output">
-          <pre class="block">{{ result.data.output }}</pre>
-      </div>
-      <div role="tabpanel" class="tab-pane" id="source">
-          <p><code>{{ script.filename }}</code></p>
-          <pre class="block">{{ script.source }}</pre>
+    <div role="tabpanel" class="tab-pane active" id="log">
+      <div class="row">
+        <div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:script_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
+          {% include 'extras/htmx/script_result.html' %}
+        </div>
       </div>
+    </div>
+    <div role="tabpanel" class="tab-pane" id="source">
+      <p><code>{{ script.filename }}</code></p>
+      <pre class="block">{{ script.source }}</pre>
+    </div>
   </div>
 {% endblock content-wrapper %}
-
-{% block data %}
-<span data-job-url="{% url 'extras-api:jobresult-detail' pk=result.pk %}"></span>
-<span data-job-complete="{{ result.completed }}"></span>
-{% endblock %}

+ 0 - 2
netbox/templates/ipam/service_edit.html

@@ -24,8 +24,6 @@
       </div>
     </div>
     <div class="tab-content p-0 border-0">
-    {{ form.initial.device }}
-    {{ form.initial.virtual_machine }}
       <div class="tab-pane {% if not form.initial.virtual_machine %}active{% endif %}" id="device" role="tabpanel" aria-labeled-by="device_tab">
         {% render_field form.device %}
       </div>

+ 3 - 5
netbox/utilities/tables.py

@@ -208,7 +208,6 @@ class ButtonsColumn(tables.TemplateColumn):
 
     :param model: Model class to use for calculating URL view names
     :param prepend_content: Additional template content to render in the column (optional)
-    :param return_url_extra: String to append to the return URL (e.g. for specifying a tab) (optional)
     """
     buttons = ('changelog', 'edit', 'delete')
     attrs = {'td': {'class': 'text-end text-nowrap noprint'}}
@@ -220,18 +219,18 @@ class ButtonsColumn(tables.TemplateColumn):
         </a>
     {{% endif %}}
     {{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
-        <a href="{{% url '{app_label}:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-sm btn-warning" title="Edit">
+        <a href="{{% url '{app_label}:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-sm btn-warning" title="Edit">
             <i class="mdi mdi-pencil"></i>
         </a>
     {{% endif %}}
     {{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
-        <a href="{{% url '{app_label}:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-sm btn-danger" title="Delete">
+        <a href="{{% url '{app_label}:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-sm btn-danger" title="Delete">
             <i class="mdi mdi-trash-can-outline"></i>
         </a>
     {{% endif %}}
     """
 
-    def __init__(self, model, *args, buttons=None, prepend_template=None, return_url_extra='', **kwargs):
+    def __init__(self, model, *args, buttons=None, prepend_template=None, **kwargs):
         if prepend_template:
             prepend_template = prepend_template.replace('{', '{{')
             prepend_template = prepend_template.replace('}', '}}')
@@ -251,7 +250,6 @@ class ButtonsColumn(tables.TemplateColumn):
 
         self.extra_context.update({
             'buttons': buttons or self.buttons,
-            'return_url_extra': return_url_extra,
         })
 
     def header(self):

Некоторые файлы не были показаны из-за большого количества измененных файлов