diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b71fb515..301fac079 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,12 +14,25 @@
-Some general tips for engaging here on GitHub: +## :information_source: Welcome to the Stadium! + +In her book [Working in Public](https://www.amazon.com/Working-Public-Making-Maintenance-Software/dp/0578675862), Nadia Eghbal defines four production models for open source projects, categorized by contributor and user growth: federations, clubs, toys, and stadiums. The NetBox project fits her definition of a stadium very well: + +> Stadiums are projects with low contributor growth and high user growth. While they may receive casual contributions, their regular contributor base does not grow proportionately to their users. As a result, they tend to be powered by one or a few developers. + +The bulk of NetBox's development is carried out by a handful of core maintainers, with occasional contributions from collaborators in the community. We find the stadium analogy very useful in conveying the roles and obligations of both contributors and users. + +If you're a contributor, actively working on the center stage, you have an obligation to produce quality content that will benefit the project as a whole. Conversely, if you're in the audience consuming the work being produced, you have the option of making requests and suggestions, but must also recognize that contributors are under no obligation to act on them. + +NetBox users are welcome to participate in either role, on stage or in the crowd. We ask only that you acknowledge the role you've chosen and respect the roles of others. + +### General Tips for Working on GitHub * Register for a free [GitHub account](https://github.com/signup) if you haven't already. * You can use [GitHub Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for formatting text and adding images. * To help mitigate notification spam, please avoid "bumping" issues with no activity. (To vote an issue up or down, use a :thumbsup: or :thumbsdown: reaction.) * Please avoid pinging members with `@` unless they've previously expressed interest or involvement with that particular issue. +* Familiarize yourself with [this list of discussion anti-patterns](https://github.com/bradfitz/issue-tracker-behaviors) and make every effort to avoid them. ## :bug: Reporting Bugs diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index efb0f44b9..000948ee7 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -43,10 +43,22 @@ Follow these instructions to perform a new installation of NetBox in a temporary Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. Once it has been merged, continue with the section for patch releases below. +### Rebuild Demo Data (After Release) + +After the release of a new minor version, generate a new demo data snapshot compatible with the new release. See the [`netbox-demo-data`](https://github.com/netbox-community/netbox-demo-data) repository for instructions. + --- ## Patch Releases +### Notify netbox-docker Project of Any Relevant Changes + +Notify the [`netbox-docker`](https://github.com/netbox-community/netbox-docker) maintainers (in **#netbox-docker**) of any changes that may be relevant to their build process, including: + +* Significant changes to `upgrade.sh` +* Increases in minimum versions for service dependencies (PostgreSQL, Redis, etc.) +* Any changes to the reference installation + ### Update Requirements Before each release, update each of NetBox's Python dependencies to its most recent stable version. These are defined in `requirements.txt`, which is updated from `base_requirements.txt` using `pip`. To do this: diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 66c2cabce..cf47a3b23 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -2,6 +2,23 @@ ## v3.5.8 (FUTURE) +### Enhancements + +* [#11675](https://github.com/netbox-community/netbox/issues/11675) - Add support for specifying import/export route targets during VRF bulk import +* [#11922](https://github.com/netbox-community/netbox/issues/11922) - Automatically populate any VDC assignments from the parent when adding a child interface via the UI +* [#12889](https://github.com/netbox-community/netbox/issues/12889) - Add 400GE CFP2 interface type +* [#13033](https://github.com/netbox-community/netbox/issues/13033) - Add human-friendly speed column to interfaces table +* [#13151](https://github.com/netbox-community/netbox/issues/13151) - Add "assigned" filter for IP addresses +* [#13368](https://github.com/netbox-community/netbox/issues/13368) - List installed plugins on the server error report page + +### Bug Fixes + +* [#12665](https://github.com/netbox-community/netbox/issues/12665) - Avoid escaping semicolons when rendering custom links +* [#12750](https://github.com/netbox-community/netbox/issues/12750) - Automatically delete an AutoSyncRecord when its object is deleted +* [#13343](https://github.com/netbox-community/netbox/issues/13343) - Fix filtering of circuits under provider network view +* [#13369](https://github.com/netbox-community/netbox/issues/13369) - Fix job termination status for failed reports +* [#13414](https://github.com/netbox-community/netbox/issues/13414) - Fix support for "hide-if-unset" custom fields on bulk import forms + --- ## v3.5.7 (2023-07-28) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index f1cfdd1d5..64dd82682 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -163,7 +163,7 @@ class ProviderNetworkView(generic.ObjectView): related_models = ( ( Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), - 'providernetwork_id', + 'provider_network_id', ), ) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 30e37e4ce..ebd66135a 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -836,6 +836,7 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_200GE_CFP2 = '200gbase-x-cfp2' TYPE_200GE_QSFP56 = '200gbase-x-qsfp56' TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd' + TYPE_400GE_CFP2 = '400gbase-x-cfp2' TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd' TYPE_400GE_OSFP = '400gbase-x-osfp' TYPE_400GE_CDFP = '400gbase-x-cdfp' @@ -978,6 +979,7 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_100GE_CFP, 'CFP (100GE)'), (TYPE_100GE_CFP2, 'CFP2 (100GE)'), (TYPE_200GE_CFP2, 'CFP2 (200GE)'), + (TYPE_400GE_CFP2, 'CFP2 (400GE)'), (TYPE_100GE_CFP4, 'CFP4 (100GE)'), (TYPE_100GE_CXP, 'CXP (100GE)'), (TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index e24154b00..879b15da2 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1098,6 +1098,9 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): queryset=VirtualDeviceContext.objects.all(), required=False, label=_('Virtual device contexts'), + initial_params={ + 'interfaces': '$parent', + }, query_params={ 'device_id': '$device', } diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 2447d447e..68c24ca14 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -591,7 +591,12 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi } ) mgmt_only = columns.BooleanColumn( - verbose_name=_('Management Only'), + verbose_name=_('Management Only') + ) + speed_formatted = columns.TemplateColumn( + template_code='{% load helpers %}{{ value|humanize_speed }}', + accessor=Accessor('speed'), + verbose_name=_('Speed') ) wireless_link = tables.Column( verbose_name=_('Wireless link'), @@ -618,7 +623,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi model = models.Interface fields = ( 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', - 'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel', + 'speed', 'speed_formatted', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c7255f5f2..e57a5834c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,4 +1,5 @@ import traceback +from collections import defaultdict from django.contrib import messages from django.contrib.contenttypes.models import ContentType @@ -45,6 +46,15 @@ CABLE_TERMINATION_TYPES = { class DeviceComponentsView(generic.ObjectChildrenView): + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename', 'bulk_disconnect') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + 'bulk_disconnect': {'change'}, + }) queryset = Device.objects.all() def get_children(self, request, parent): @@ -1997,6 +2007,7 @@ class DeviceModuleBaysView(DeviceComponentsView): table = tables.DeviceModuleBayTable filterset = filtersets.ModuleBayFilterSet template_name = 'dcim/device/modulebays.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') tab = ViewTab( label=_('Module Bays'), badge=lambda obj: obj.module_bay_count, @@ -2012,6 +2023,7 @@ class DeviceDeviceBaysView(DeviceComponentsView): table = tables.DeviceDeviceBayTable filterset = filtersets.DeviceBayFilterSet template_name = 'dcim/device/devicebays.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') tab = ViewTab( label=_('Device Bays'), badge=lambda obj: obj.device_bay_count, @@ -2023,6 +2035,7 @@ class DeviceDeviceBaysView(DeviceComponentsView): @register_model_view(Device, 'inventory') class DeviceInventoryView(DeviceComponentsView): + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') child_model = InventoryItem table = tables.DeviceInventoryItemTable filterset = filtersets.InventoryItemFilterSet diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 082891392..54832b3ba 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -316,7 +316,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): text = clean_html(text, allowed_schemes) # Sanitize link - link = urllib.parse.quote(link, safe='/:?&=%+[]@#,') + link = urllib.parse.quote(link, safe='/:?&=%+[]@#,;') # Verify link scheme is allowed result = urllib.parse.urlparse(link) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 83c7a7bb0..8736a3197 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -2,7 +2,6 @@ import collections from importlib import import_module from django.apps import AppConfig -from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils.module_loading import import_string from packaging import version @@ -146,23 +145,3 @@ class PluginConfig(AppConfig): for setting, value in cls.default_settings.items(): if setting not in user_config: user_config[setting] = value - - -# -# Utilities -# - -def get_plugin_config(plugin_name, parameter, default=None): - """ - Return the value of the specified plugin configuration parameter. - - Args: - plugin_name: The name of the plugin - parameter: The name of the configuration parameter - default: The value to return if the parameter is not defined (default: None) - """ - try: - plugin_config = settings.PLUGINS_CONFIG[plugin_name] - return plugin_config.get(parameter, default) - except KeyError: - raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.") diff --git a/netbox/extras/plugins/utils.py b/netbox/extras/plugins/utils.py new file mode 100644 index 000000000..c260f156d --- /dev/null +++ b/netbox/extras/plugins/utils.py @@ -0,0 +1,37 @@ +from django.apps import apps +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +__all__ = ( + 'get_installed_plugins', + 'get_plugin_config', +) + + +def get_installed_plugins(): + """ + Return a dictionary mapping the names of installed plugins to their versions. + """ + plugins = {} + for plugin_name in settings.PLUGINS: + plugin_name = plugin_name.rsplit('.', 1)[-1] + plugin_config = apps.get_app_config(plugin_name) + plugins[plugin_name] = getattr(plugin_config, 'version', None) + + return dict(sorted(plugins.items())) + + +def get_plugin_config(plugin_name, parameter, default=None): + """ + Return the value of the specified plugin configuration parameter. + + Args: + plugin_name: The name of the plugin + parameter: The name of the configuration parameter + default: The value to return if the parameter is not defined (default: None) + """ + try: + plugin_config = settings.PLUGINS_CONFIG[plugin_name] + return plugin_config.get(parameter, default) + except KeyError: + raise ImproperlyConfigured(f"Plugin {plugin_name} is not registered.") diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index 8f3af2a09..6af81a9d9 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -214,20 +214,18 @@ class Report(object): self.active_test = method_name test_method = getattr(self, method_name) test_method() + job.data = self._results if self.failed: self.logger.warning("Report failed") - job.status = JobStatusChoices.STATUS_FAILED + job.terminate(status=JobStatusChoices.STATUS_FAILED) else: self.logger.info("Report completed successfully") - job.status = JobStatusChoices.STATUS_COMPLETED + job.terminate() except Exception as e: stacktrace = traceback.format_exc() self.log_failure(None, f"An exception occurred: {type(e).__name__}: {e}{stacktrace}") logger.error(f"Exception raised during report execution: {e}") job.terminate(status=JobStatusChoices.STATUS_ERRORED) - finally: - job.data = self._results - job.terminate() # Perform any post-run tasks self.post_run() diff --git a/netbox/extras/tests/test_plugins.py b/netbox/extras/tests/test_plugins.py index cb7629ad2..42dde43fd 100644 --- a/netbox/extras/tests/test_plugins.py +++ b/netbox/extras/tests/test_plugins.py @@ -5,8 +5,9 @@ from django.core.exceptions import ImproperlyConfigured from django.test import Client, TestCase, override_settings from django.urls import reverse -from extras.plugins import PluginMenu, get_plugin_config +from extras.plugins import PluginMenu from extras.tests.dummy_plugin import config as dummy_config +from extras.plugins.utils import get_plugin_config from netbox.graphql.schema import Query from netbox.registry import registry diff --git a/netbox/extras/tests/test_webhooks.py b/netbox/extras/tests/test_webhooks.py index 19264dabb..ef7637765 100644 --- a/netbox/extras/tests/test_webhooks.py +++ b/netbox/extras/tests/test_webhooks.py @@ -31,8 +31,8 @@ class WebhookTest(APITestCase): def setUpTestData(cls): site_ct = ContentType.objects.get_for_model(Site) - DUMMY_URL = "http://localhost/" - DUMMY_SECRET = "LOOKATMEIMASECRETSTRING" + DUMMY_URL = 'http://localhost:9000/' + DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING' webhooks = Webhook.objects.bulk_create(( Webhook(name='Webhook 1', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'), @@ -259,7 +259,7 @@ class WebhookTest(APITestCase): name='Conditional Webhook', type_create=True, type_update=True, - payload_url='http://localhost/', + payload_url='http://localhost:9000/', conditions={ 'and': [ { diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index d011472d9..9b57cb273 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -591,6 +591,10 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): method='_assigned_to_interface', label=_('Is assigned to an interface'), ) + assigned = django_filters.BooleanFilter( + method='_assigned', + label=_('Is assigned'), + ) status = django_filters.MultipleChoiceFilter( choices=IPAddressStatusChoices, null_value=None @@ -706,6 +710,18 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet): assigned_object_id__isnull=False ) + def _assigned(self, queryset, name, value): + if value: + return queryset.exclude( + assigned_object_type__isnull=True, + assigned_object_id__isnull=True + ) + else: + return queryset.filter( + assigned_object_type__isnull=True, + assigned_object_id__isnull=True + ) + class FHRPGroupFilterSet(NetBoxModelFilterSet): protocol = django_filters.MultipleChoiceFilter( diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 9b7c624d2..ec0812593 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -1,7 +1,6 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from dcim.models import Device, Interface, Site @@ -10,7 +9,9 @@ from ipam.constants import * from ipam.models import * from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField +from utilities.forms.fields import ( + CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, SlugField +) from virtualization.models import VirtualMachine, VMInterface __all__ = ( @@ -42,10 +43,25 @@ class VRFImportForm(NetBoxModelImportForm): to_field_name='name', help_text=_('Assigned tenant') ) + import_targets = CSVModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False, + to_field_name='name', + help_text=_('Import route targets') + ) + export_targets = CSVModelMultipleChoiceField( + queryset=RouteTarget.objects.all(), + required=False, + to_field_name='name', + help_text=_('Export route targets') + ) class Meta: model = VRF - fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', 'tags') + fields = ( + 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'comments', + 'tags', + ) class RouteTargetImportForm(NetBoxModelImportForm): diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 0e17a9ce2..4adff78ef 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -992,6 +992,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'fhrpgroup_id': [fhrp_groups[0].pk, fhrp_groups[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_assigned(self): + params = {'assigned': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + params = {'assigned': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_assigned_to_interface(self): params = {'assigned_to_interface': 'true'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 32badd2d5..d8e4d8b47 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -216,7 +216,7 @@ class ASNRangeASNsView(generic.ObjectChildrenView): child_model = ASN table = tables.ASNTable filterset = filtersets.ASNFilterSet - template_name = 'ipam/asnrange/asns.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('ASNs'), badge=lambda x: x.get_child_asns().count(), @@ -816,7 +816,6 @@ class IPAddressAssignView(generic.ObjectView): table = None if form.is_valid(): - addresses = self.queryset.prefetch_related('vrf', 'tenant') # Limit to 100 results addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100] @@ -866,7 +865,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView): child_model = IPAddress table = tables.IPAddressTable filterset = filtersets.IPAddressFilterSet - template_name = 'ipam/ipaddress/ip_addresses.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('Related IPs'), badge=lambda x: x.get_related_ips().count(), @@ -963,7 +962,6 @@ class FHRPGroupView(generic.ObjectView): queryset = FHRPGroup.objects.all() def get_extra_context(self, request, instance): - # Get assigned interfaces members_table = tables.FHRPGroupAssignmentTable( data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance), @@ -1077,7 +1075,7 @@ class VLANInterfacesView(generic.ObjectChildrenView): child_model = Interface table = tables.VLANDevicesTable filterset = InterfaceFilterSet - template_name = 'ipam/vlan/interfaces.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('Device Interfaces'), badge=lambda x: x.get_interfaces().count(), @@ -1095,7 +1093,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView): child_model = VMInterface table = tables.VLANVirtualMachinesTable filterset = VMInterfaceFilterSet - template_name = 'ipam/vlan/vminterfaces.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('VM Interfaces'), badge=lambda x: x.get_vminterfaces().count(), diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 5c55697ff..97f690762 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -11,6 +11,7 @@ from rest_framework.reverse import reverse from rest_framework.views import APIView from rq.worker import Worker +from extras.plugins.utils import get_installed_plugins from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired @@ -61,19 +62,11 @@ class StatusView(APIView): installed_apps[app_config.name] = version installed_apps = {k: v for k, v in sorted(installed_apps.items())} - # Gather installed plugins - plugins = {} - for plugin_name in settings.PLUGINS: - plugin_name = plugin_name.rsplit('.', 1)[-1] - plugin_config = apps.get_app_config(plugin_name) - plugins[plugin_name] = getattr(plugin_config, 'version', None) - plugins = {k: v for k, v in sorted(plugins.items())} - return Response({ 'django-version': DJANGO_VERSION, 'installed-apps': installed_apps, 'netbox-version': settings.VERSION, - 'plugins': plugins, + 'plugins': get_installed_plugins(), 'python-version': platform.python_version(), 'rq-workers-running': Worker.count(get_connection('default')), }) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index f28e74eb5..c5dac90f7 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -88,7 +88,10 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): def _get_custom_fields(self, content_type): return CustomField.objects.filter(content_types=content_type).filter( - ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE + ui_visibility__in=[ + CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, + CustomFieldVisibilityChoices.VISIBILITY_HIDDEN_IFUNSET, + ] ) def _get_form_field(self, customfield): diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index f0ab29123..cce265efc 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -491,6 +491,19 @@ class SyncedDataMixin(models.Model): return ret + def delete(self, *args, **kwargs): + from core.models import AutoSyncRecord + + # Delete AutoSyncRecord + content_type = ContentType.objects.get_for_model(self) + AutoSyncRecord.objects.filter( + datafile=self.data_file, + object_type=content_type, + object_id=self.pk + ).delete() + + return super().delete(*args, **kwargs) + def resolve_data_file(self): """ Determine the designated DataFile object identified by its parent DataSource and its path. Returns None if diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 1fae55f2e..92a556ed2 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -474,8 +474,6 @@ LOGIN_REDIRECT_URL = f'/{BASE_PATH}' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -TEST_RUNNER = "django_rich.test.RichRunner" - # Exclude potentially sensitive models from wildcard view exemption. These may still be exempted # by specifying the model individually in the EXEMPT_VIEW_PERMISSIONS configuration parameter. EXEMPT_EXCLUDE_MODELS = ( diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 673c01185..52ff69aa9 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -54,7 +54,7 @@ class BaseTable(tables.Table): # 3. Meta.fields selected_columns = None if user is not None and not isinstance(user, AnonymousUser): - selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns") + selected_columns = user.config.get(f"tables.{self.name}.columns") if not selected_columns: selected_columns = getattr(self.Meta, 'default_columns', self.Meta.fields) @@ -113,6 +113,10 @@ class BaseTable(tables.Table): columns.append((name, column.verbose_name)) return columns + @property + def name(self): + return self.__class__.__name__ + @property def available_columns(self): return self._get_columns(visible=False) @@ -138,17 +142,16 @@ class BaseTable(tables.Table): """ # Save ordering preference if request.user.is_authenticated: - table_name = self.__class__.__name__ if self.prefixed_order_by_field in request.GET: if request.GET[self.prefixed_order_by_field]: # If an ordering has been specified as a query parameter, save it as the # user's preferred ordering for this table. ordering = request.GET.getlist(self.prefixed_order_by_field) - request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True) + request.user.config.set(f'tables.{self.name}.ordering', ordering, commit=True) else: # If the ordering has been set to none (empty), clear any existing preference. - request.user.config.clear(f'tables.{table_name}.ordering', commit=True) - elif ordering := request.user.config.get(f'tables.{table_name}.ordering'): + request.user.config.clear(f'tables.{self.name}.ordering', commit=True) + elif ordering := request.user.config.get(f'tables.{self.name}.ordering'): # If no ordering has been specified, set the preferred ordering (if any). self.order_by = ordering diff --git a/netbox/netbox/views/errors.py b/netbox/netbox/views/errors.py index c74c67cef..a81d45cb5 100644 --- a/netbox/netbox/views/errors.py +++ b/netbox/netbox/views/errors.py @@ -11,6 +11,8 @@ from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found from django.views.generic import View from sentry_sdk import capture_message +from extras.plugins.utils import get_installed_plugins + __all__ = ( 'handler_404', 'handler_500', @@ -53,4 +55,5 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME): 'exception': str(type_), 'netbox_version': settings.VERSION, 'python_version': platform.python_version(), + 'plugins': get_installed_plugins(), })) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 1ba789cf1..99d8ff540 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -143,9 +143,12 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin): return render(request, self.get_template_name(), { 'object': instance, 'child_model': self.child_model, + 'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html', 'table': table, + 'table_config': f'{table.name}_config', 'actions': actions, 'tab': self.tab, + 'return_url': request.get_full_path(), **self.get_extra_context(request, instance), }) diff --git a/netbox/templates/500.html b/netbox/templates/500.html index f3354688e..b26b61973 100644 --- a/netbox/templates/500.html +++ b/netbox/templates/500.html @@ -31,7 +31,10 @@ {{ error }} {% trans "Python version" %}: {{ python_version }} -{% trans "NetBox version" %}: {{ netbox_version }} +{% trans "NetBox version" %}: {{ netbox_version }} +{% trans "Plugins" %}: {% for plugin, version in plugins.items %} + {{ plugin }}: {{ version }}{% empty %}{% trans "None installed" %}{% endfor %} +
{% trans "If further assistance is required, please post to the" %} {% trans "NetBox discussion forum" %} {% trans "on GitHub" %}.
diff --git a/netbox/templates/dcim/device/components_base.html b/netbox/templates/dcim/device/components_base.html new file mode 100644 index 000000000..1e3d8a39d --- /dev/null +++ b/netbox/templates/dcim/device/components_base.html @@ -0,0 +1,15 @@ +{% extends 'generic/object_children.html' %} +{% load helpers %} + +{% block bulk_edit_controls %} + {{ block.super }} + {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %} + {% if 'bulk_rename' in actions and bulk_rename_view %} + + {% endif %} + {% endwith %} +{% endblock bulk_edit_controls %} diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html index 1df555cda..90f4f99ed 100644 --- a/netbox/templates/dcim/device/consoleports.html +++ b/netbox/templates/dcim/device/consoleports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_consoleport %} + + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html index c7b818d29..39289f2e5 100644 --- a/netbox/templates/dcim/device/consoleserverports.html +++ b/netbox/templates/dcim/device/consoleserverports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_consoleserverport %} + + {% endif %} +{% endblock bulk_extra_controls %} \ No newline at end of file diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html index 2f659af9a..5b2a396d7 100644 --- a/netbox/templates/dcim/device/devicebays.html +++ b/netbox/templates/dcim/device/devicebays.html @@ -1,51 +1,14 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} -{% load helpers %} -{% load static %} +{% extends 'dcim/device/components_base.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html index efc632e75..2edef9894 100644 --- a/netbox/templates/dcim/device/frontports.html +++ b/netbox/templates/dcim/device/frontports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_frontport %} + + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 8baf0786b..cab46886b 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -1,67 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html index ceebc50ee..15d70d959 100644 --- a/netbox/templates/dcim/device/inventory.html +++ b/netbox/templates/dcim/device/inventory.html @@ -1,51 +1,14 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} -{% load helpers %} -{% load static %} +{% extends 'dcim/device/components_base.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/modulebays.html b/netbox/templates/dcim/device/modulebays.html index b5f1df526..b218242be 100644 --- a/netbox/templates/dcim/device/modulebays.html +++ b/netbox/templates/dcim/device/modulebays.html @@ -1,47 +1,14 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} -{% load helpers %} -{% load static %} +{% extends 'dcim/device/components_base.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %} - - - {% table_config_form table %} -{% endblock %} + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html index 41960585b..34f51e389 100644 --- a/netbox/templates/dcim/device/poweroutlets.html +++ b/netbox/templates/dcim/device/poweroutlets.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_poweroutlet %} + + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html index 5301319da..ee9d57fd2 100644 --- a/netbox/templates/dcim/device/powerports.html +++ b/netbox/templates/dcim/device/powerports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} -{% load static %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_powerport %} + + {% endif %} +{% endblock bulk_extra_controls %} diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html index e81d915be..0c7781005 100644 --- a/netbox/templates/dcim/device/rearports.html +++ b/netbox/templates/dcim/device/rearports.html @@ -1,58 +1,28 @@ -{% extends 'dcim/device/base.html' %} -{% load render_table from django_tables2 %} -{% load static %} +{% extends 'dcim/device/components_base.html' %} {% load helpers %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %} - - -{% endblock %} + {% endwith %} +{% endblock bulk_delete_controls %} -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_extra_controls %} + {{ block.super }} + {% if perms.dcim.add_rearport %} + + {% endif %} +{% endblock bulk_extra_controls %} \ No newline at end of file diff --git a/netbox/templates/dcim/rack/non_racked_devices.html b/netbox/templates/dcim/rack/non_racked_devices.html index 700c66369..e52b8647f 100644 --- a/netbox/templates/dcim/rack/non_racked_devices.html +++ b/netbox/templates/dcim/rack/non_racked_devices.html @@ -1,5 +1,4 @@ -{% extends 'dcim/rack/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% block extra_controls %} {% if perms.dcim.add_device %} @@ -10,42 +9,4 @@ {% endif %} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/dcim/rack/reservations.html b/netbox/templates/dcim/rack/reservations.html index 1995d6174..cca8800e7 100644 --- a/netbox/templates/dcim/rack/reservations.html +++ b/netbox/templates/dcim/rack/reservations.html @@ -1,44 +1,13 @@ -{% extends 'dcim/rack/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="RackReservationTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} + {% endif %} +{% endblock extra_controls %} diff --git a/netbox/templates/generic/object_children.html b/netbox/templates/generic/object_children.html new file mode 100644 index 000000000..eb5c65827 --- /dev/null +++ b/netbox/templates/generic/object_children.html @@ -0,0 +1,57 @@ +{% extends base_template %} +{% load helpers %} + +{% block content %} + {% include 'inc/table_controls_htmx.html' with table_modal=table_config %} + +{% endblock content %} + +{% block modals %} + {{ block.super }} + {% table_config_form table %} +{% endblock modals %} diff --git a/netbox/templates/ipam/aggregate/prefixes.html b/netbox/templates/ipam/aggregate/prefixes.html index 37084f9f3..e1e53962d 100644 --- a/netbox/templates/ipam/aggregate/prefixes.html +++ b/netbox/templates/ipam/aggregate/prefixes.html @@ -1,5 +1,4 @@ -{% extends 'ipam/aggregate/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} @@ -10,38 +9,4 @@ {% endif %} {{ block.super }} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/asnrange/asns.html b/netbox/templates/ipam/asnrange/asns.html deleted file mode 100644 index c0f2a15ba..000000000 --- a/netbox/templates/ipam/asnrange/asns.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'ipam/asnrange/base.html' %} -{% load helpers %} -{% load i18n %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="ASNTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/ipam/ipaddress/ip_addresses.html b/netbox/templates/ipam/ipaddress/ip_addresses.html deleted file mode 100644 index b82ec2375..000000000 --- a/netbox/templates/ipam/ipaddress/ip_addresses.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'ipam/ipaddress/base.html' %} -{% load helpers %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %} - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/ipam/iprange/ip_addresses.html b/netbox/templates/ipam/iprange/ip_addresses.html index f3bc0666f..a9baccc39 100644 --- a/netbox/templates/ipam/iprange/ip_addresses.html +++ b/netbox/templates/ipam/iprange/ip_addresses.html @@ -1,45 +1,10 @@ -{% extends 'ipam/iprange/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} - {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %} + {% if perms.ipam.add_ipaddress and object.first_available_ip %} {% trans "Add IP Address" %} {% endif %} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/prefix/ip_addresses.html b/netbox/templates/ipam/prefix/ip_addresses.html index ac02590f6..51ebcc730 100644 --- a/netbox/templates/ipam/prefix/ip_addresses.html +++ b/netbox/templates/ipam/prefix/ip_addresses.html @@ -1,5 +1,4 @@ -{% extends 'ipam/prefix/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} @@ -8,38 +7,4 @@ {% trans "Add IP Address" %} {% endif %} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/prefix/ip_ranges.html b/netbox/templates/ipam/prefix/ip_ranges.html index 6d2910c64..3bfba921f 100644 --- a/netbox/templates/ipam/prefix/ip_ranges.html +++ b/netbox/templates/ipam/prefix/ip_ranges.html @@ -1,5 +1,4 @@ -{% extends 'ipam/prefix/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} @@ -8,38 +7,4 @@ {% trans "Add IP Range" %} {% endif %} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/prefix/prefixes.html b/netbox/templates/ipam/prefix/prefixes.html index 189a714e1..b8c2fd31c 100644 --- a/netbox/templates/ipam/prefix/prefixes.html +++ b/netbox/templates/ipam/prefix/prefixes.html @@ -1,5 +1,4 @@ -{% extends 'ipam/prefix/base.html' %} -{% load helpers %} +{% extends 'generic/object_children.html' %} {% load i18n %} {% block extra_controls %} @@ -9,39 +8,4 @@ {% trans "Add Prefix" %} {% endif %} - {{ block.super }} -{% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %} - - -{% endblock %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% endblock extra_controls %} diff --git a/netbox/templates/ipam/vlan/interfaces.html b/netbox/templates/ipam/vlan/interfaces.html deleted file mode 100644 index f7bcc8563..000000000 --- a/netbox/templates/ipam/vlan/interfaces.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'ipam/vlan/base.html' %} -{% load helpers %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/ipam/vlan/vminterfaces.html b/netbox/templates/ipam/vlan/vminterfaces.html deleted file mode 100644 index a485b33eb..000000000 --- a/netbox/templates/ipam/vlan/vminterfaces.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'ipam/vlan/base.html' %} -{% load helpers %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/tenancy/object_contacts.html b/netbox/templates/tenancy/object_contacts.html index 47f9f57bc..6e4814d8e 100644 --- a/netbox/templates/tenancy/object_contacts.html +++ b/netbox/templates/tenancy/object_contacts.html @@ -1,4 +1,4 @@ -{% extends base_template %} +{% extends 'generic/object_children.html' %} {% load helpers %} {% load i18n %} @@ -11,20 +11,3 @@ {% endwith %} {% endif %} {% endblock %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="ContactAssignmentTable_config" %} - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/virtualization/cluster/devices.html b/netbox/templates/virtualization/cluster/devices.html index 077611e24..caf0af0de 100644 --- a/netbox/templates/virtualization/cluster/devices.html +++ b/netbox/templates/virtualization/cluster/devices.html @@ -1,31 +1,13 @@ -{% extends 'virtualization/cluster/base.html' %} -{% load helpers %} -{% load render_table from django_tables2 %} +{% extends 'generic/object_children.html' %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} +{% block bulk_delete_controls %} + {{ block.super }} + {% if 'bulk_remove_devices' in actions %} + + {% endif %} +{% endblock bulk_delete_controls %} diff --git a/netbox/templates/virtualization/cluster/virtual_machines.html b/netbox/templates/virtualization/cluster/virtual_machines.html deleted file mode 100644 index 2c83382fc..000000000 --- a/netbox/templates/virtualization/cluster/virtual_machines.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends 'virtualization/cluster/base.html' %} -{% load helpers %} -{% load render_table from django_tables2 %} -{% load i18n %} - -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} diff --git a/netbox/templates/virtualization/virtualmachine/interfaces.html b/netbox/templates/virtualization/virtualmachine/interfaces.html index eb39a0418..b68872da8 100644 --- a/netbox/templates/virtualization/virtualmachine/interfaces.html +++ b/netbox/templates/virtualization/virtualmachine/interfaces.html @@ -1,48 +1,14 @@ -{% extends 'virtualization/virtualmachine/base.html' %} -{% load render_table from django_tables2 %} +{% extends 'generic/object_children.html' %} {% load helpers %} {% load i18n %} -{% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %} - - -{% endblock content %} - -{% block modals %} - {{ block.super }} - {% table_config_form table %} -{% endblock modals %} + {% endif %} +{% endblock bulk_edit_controls %} diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 3b46b5514..76a86146c 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -41,11 +41,6 @@ class ObjectContactsView(generic.ObjectChildrenView): return table - def get_extra_context(self, request, instance): - return { - 'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html', - } - # # Tenant groups # diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index c56a8ade2..a4474610a 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -1,3 +1,5 @@ +from collections import defaultdict + from django.contrib import messages from django.db import transaction from django.db.models import Prefetch, Sum @@ -175,7 +177,7 @@ class ClusterVirtualMachinesView(generic.ObjectChildrenView): child_model = VirtualMachine table = tables.VirtualMachineTable filterset = filtersets.VirtualMachineFilterSet - template_name = 'virtualization/cluster/virtual_machines.html' + template_name = 'generic/object_children.html' tab = ViewTab( label=_('Virtual Machines'), badge=lambda obj: obj.virtual_machines.count(), @@ -194,6 +196,13 @@ class ClusterDevicesView(generic.ObjectChildrenView): table = DeviceTable filterset = DeviceFilterSet template_name = 'virtualization/cluster/devices.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_remove_devices') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_remove_devices': {'change'}, + }) tab = ViewTab( label=_('Devices'), badge=lambda obj: obj.devices.count(), @@ -353,6 +362,14 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView): permission='virtualization.view_vminterface', weight=500 ) + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) def get_children(self, request, parent): return parent.interfaces.restrict(request.user, 'view').prefetch_related(