Merge branch 'develop' into 16424-allow_filtering_of_devices_by_cluster_and_cluster_group

This commit is contained in:
Julio-Oliveira-Encora 2024-06-19 09:12:33 -03:00
commit 40c4b17d2a
36 changed files with 471 additions and 398 deletions

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ netbox.pid
.idea .idea
.coverage .coverage
.vscode .vscode
.python-version

View File

@ -126,3 +126,13 @@ VERSION = 'v3.3.2-dev'
``` ```
Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream. Commit this change with the comment "PRVB" (for _post-release version bump_) and push the commit upstream.
### Update the Public Documentation
After a release has been published, the public NetBox documentation needs to be updated. This is accomplished by running two actions on the [netboxlabs-docs](https://github.com/netboxlabs/netboxlabs-docs) repository.
First, run the `build-site` action, by navigating to Actions > build-site > Run workflow. This process compiles the documentation along with an overlay for integration with the documentation portal at <https://netboxlabs.com/docs>. The job should take about two minutes.
Once the documentation files have been compiled, they must be published by running the `deploy-kinsta` action. Select the desired deployment environment (staging or production) and specify `latest` as the deploy tag.
Finally, verify that the documentation at <https://netboxlabs.com/docs/netbox/en/stable/> has been updated.

View File

@ -2,6 +2,26 @@
## v4.0.6 (FUTURE) ## v4.0.6 (FUTURE)
### Enhancements
* [#15348](https://github.com/netbox-community/netbox/issues/15348) - Show saved filters alongside quick search on object list views
* [#15794](https://github.com/netbox-community/netbox/issues/15794) - Dynamically populate related objects in UI views
* [#16256](https://github.com/netbox-community/netbox/issues/16256) - Enable alphabetical ordering of bookmarks on dashboard
### Bug Fixes
* [#13925](https://github.com/netbox-community/netbox/issues/13925) - Fix support for "zulu" (UTC) timestamps for custom fields
* [#14829](https://github.com/netbox-community/netbox/issues/14829) - Fix support for simple conditions (without AND/OR) in event rules
* [#16143](https://github.com/netbox-community/netbox/issues/16143) - Display timestamps in tables in the configured timezone
* [#16416](https://github.com/netbox-community/netbox/issues/16416) - Retain dark/light mode toggle on mobile view
* [#16444](https://github.com/netbox-community/netbox/issues/16444) - Disable ordering circuits list by A/Z termination
* [#16450](https://github.com/netbox-community/netbox/issues/16450) - Searching for rack unit in form dropdown should be case-insensitive
* [#16452](https://github.com/netbox-community/netbox/issues/16452) - Fix sizing of buttons within object attribute panels
* [#16454](https://github.com/netbox-community/netbox/issues/16454) - Address DNS lookup bug in `django-debug-toolbar
* [#16460](https://github.com/netbox-community/netbox/issues/16460) - Omit spaces from telephone number URLs
* [#16512](https://github.com/netbox-community/netbox/issues/16512) - Restore a user's preferred language (if any) on login
* [#16542](https://github.com/netbox-community/netbox/issues/16542) - Fix bulk form operations when HTMX is enabled
--- ---
## v4.0.5 (2024-06-06) ## v4.0.5 (2024-06-06)

View File

@ -104,10 +104,16 @@ class LoginView(View):
# Ensure the user has a UserConfig defined. (This should normally be handled by # Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.) # create_userconfig() on user creation.)
if not hasattr(request.user, 'config'): if not hasattr(request.user, 'config'):
config = get_config() request.user.config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save() UserConfig(user=request.user, data=request.user.config.DEFAULT_USER_PREFERENCES).save()
return self.redirect_to_next(request, logger) response = self.redirect_to_next(request, logger)
# Set the user's preferred language (if any)
if language := request.user.config.get('locale.language'):
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
return response
else: else:
logger.debug(f"Login form validation failed for username: {form['username'].value()}") logger.debug(f"Login form validation failed for username: {form['username'].value()}")
@ -145,9 +151,10 @@ class LogoutView(View):
logger.info(f"User {username} has logged out") logger.info(f"User {username} has logged out")
messages.info(request, "You have logged out.") messages.info(request, "You have logged out.")
# Delete session key cookie (if set) upon logout # Delete session key & language cookies (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL)) response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
response.delete_cookie('session_key') response.delete_cookie('session_key')
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
return response return response

View File

@ -63,10 +63,12 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
termination_a = tables.TemplateColumn( termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK, template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side A') verbose_name=_('Side A')
) )
termination_z = tables.TemplateColumn( termination_z = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK, template_code=CIRCUITTERMINATION_LINK,
orderable=False,
verbose_name=_('Side Z') verbose_name=_('Side Z')
) )
commit_rate = CommitRateColumn( commit_rate = CommitRateColumn(

View File

@ -7,7 +7,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.query import count_related from utilities.query import count_related
from utilities.views import register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -26,17 +26,12 @@ class ProviderListView(generic.ObjectListView):
@register_model_view(Provider) @register_model_view(Provider)
class ProviderView(generic.ObjectView): class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
(Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -92,16 +87,12 @@ class ProviderAccountListView(generic.ObjectListView):
@register_model_view(ProviderAccount) @register_model_view(ProviderAccount)
class ProviderAccountView(generic.ObjectView): class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderAccount.objects.all() queryset = ProviderAccount.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -156,19 +147,21 @@ class ProviderNetworkListView(generic.ObjectListView):
@register_model_view(ProviderNetwork) @register_model_view(ProviderNetwork)
class ProviderNetworkView(generic.ObjectView): class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderNetwork.objects.all() queryset = ProviderNetwork.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = ( return {
'related_models': self.get_related_models(
request,
instance,
extra=(
( (
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
'provider_network_id', 'provider_network_id',
), ),
) ),
),
return {
'related_models': related_models,
} }
@ -215,16 +208,12 @@ class CircuitTypeListView(generic.ObjectListView):
@register_model_view(CircuitType) @register_model_view(CircuitType)
class CircuitTypeView(generic.ObjectView): class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -32,7 +32,7 @@ from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.htmx import htmx_partial from utilities.htmx import htmx_partial
from utilities.query import count_related from utilities.query import count_related
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -51,16 +51,12 @@ class DataSourceListView(generic.ObjectListView):
@register_model_view(DataSource) @register_model_view(DataSource)
class DataSourceView(generic.ObjectView): class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DataSource.objects.all() queryset = DataSource.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(DataFile.objects.restrict(request.user, 'view').filter(source=instance), 'source_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -219,9 +219,9 @@ class RackViewSet(NetBoxModelViewSet):
) )
# Enable filtering rack units by ID # Enable filtering rack units by ID
q = data['q'] if q := data['q']:
if q: q = q.lower()
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name'])] elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name']).lower()]
page = self.paginate_queryset(elevation) page = self.paginate_queryset(elevation)
if page is not None: if page is not None:

View File

@ -17,7 +17,7 @@ from jinja2.exceptions import TemplateError
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup from ipam.models import ASN, IPAddress, VLANGroup
from ipam.tables import InterfaceVLANTable from ipam.tables import InterfaceVLANTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic from netbox.views import generic
@ -27,7 +27,9 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.query import count_related from utilities.query import count_related
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view from utilities.views import (
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
)
from virtualization.filtersets import VirtualMachineFilterSet from virtualization.filtersets import VirtualMachineFilterSet
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from virtualization.tables import VirtualMachineTable from virtualization.tables import VirtualMachineTable
@ -226,19 +228,21 @@ class RegionListView(generic.ObjectListView):
@register_model_view(Region) @register_model_view(Region)
class RegionView(generic.ObjectView): class RegionView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Region.objects.all() queryset = Region.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
regions = instance.get_descendants(include_self=True) regions = instance.get_descendants(include_self=True)
related_models = (
(Site.objects.restrict(request.user, 'view').filter(region__in=regions), 'region_id'),
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(
request,
regions,
extra=(
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
),
),
} }
@ -306,19 +310,21 @@ class SiteGroupListView(generic.ObjectListView):
@register_model_view(SiteGroup) @register_model_view(SiteGroup)
class SiteGroupView(generic.ObjectView): class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = SiteGroup.objects.all() queryset = SiteGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
related_models = (
(Site.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(
request,
groups,
extra=(
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
),
),
} }
@ -380,31 +386,25 @@ class SiteListView(generic.ObjectListView):
@register_model_view(Site) @register_model_view(Site)
class SiteView(generic.ObjectView): class SiteView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Site.objects.prefetch_related('tenant__group') queryset = Site.objects.prefetch_related('tenant__group')
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = ( return {
# DCIM 'related_models': self.get_related_models(
(Location.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), request,
(Rack.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), instance,
(Device.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), [CableTermination, CircuitTermination],
# Virtualization (
(VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance), 'site_id'),
# IPAM
(Prefix.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'),
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
(VLANGroup.objects.restrict(request.user, 'view').filter( (VLANGroup.objects.restrict(request.user, 'view').filter(
scope_type=ContentType.objects.get_for_model(Site), scope_type=ContentType.objects.get_for_model(Site),
scope_id=instance.pk scope_id=instance.pk
), 'site'), ), 'site'),
(VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), (ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
# Circuits (Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(),
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'), 'site_id'),
) ),
),
return {
'related_models': related_models,
} }
@ -466,18 +466,13 @@ class LocationListView(generic.ObjectListView):
@register_model_view(Location) @register_model_view(Location)
class LocationView(generic.ObjectView): class LocationView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Location.objects.all() queryset = Location.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
locations = instance.get_descendants(include_self=True) locations = instance.get_descendants(include_self=True)
related_models = (
(Rack.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
(Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, locations, [CableTermination]),
} }
@ -541,16 +536,12 @@ class RackRoleListView(generic.ObjectListView):
@register_model_view(RackRole) @register_model_view(RackRole)
class RackRoleView(generic.ObjectView): class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RackRole.objects.all() queryset = RackRole.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Rack.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -655,15 +646,10 @@ class RackElevationListView(generic.ObjectListView):
@register_model_view(Rack) @register_model_view(Rack)
class RackView(generic.ObjectView): class RackView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role') queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user, 'view').filter(rack=instance), 'rack_id'),
(PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'),
)
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site) peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
if instance.location: if instance.location:
@ -679,7 +665,7 @@ class RackView(generic.ObjectView):
]) ])
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, [CableTermination]),
'next_rack': next_rack, 'next_rack': next_rack,
'prev_rack': prev_rack, 'prev_rack': prev_rack,
'svg_extra': svg_extra, 'svg_extra': svg_extra,
@ -838,19 +824,12 @@ class ManufacturerListView(generic.ObjectListView):
@register_model_view(Manufacturer) @register_model_view(Manufacturer)
class ManufacturerView(generic.ObjectView): class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Manufacturer.objects.all() queryset = Manufacturer.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(DeviceType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
(ModuleType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
(InventoryItem.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
(Platform.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, [InventoryItemTemplate]),
} }
@ -912,16 +891,16 @@ class DeviceTypeListView(generic.ObjectListView):
@register_model_view(DeviceType) @register_model_view(DeviceType)
class DeviceTypeView(generic.ObjectView): class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user).filter(device_type=instance), 'device_type_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, omit=[
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
]),
} }
@ -1151,16 +1130,16 @@ class ModuleTypeListView(generic.ObjectListView):
@register_model_view(ModuleType) @register_model_view(ModuleType)
class ModuleTypeView(generic.ObjectView): class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Module.objects.restrict(request.user).filter(module_type=instance), 'module_type_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, omit=[
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
]),
} }
@ -1711,17 +1690,12 @@ class DeviceRoleListView(generic.ObjectListView):
@register_model_view(DeviceRole) @register_model_view(DeviceRole)
class DeviceRoleView(generic.ObjectView): class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
(VirtualMachine.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -1775,17 +1749,12 @@ class PlatformListView(generic.ObjectListView):
@register_model_view(Platform) @register_model_view(Platform)
class PlatformView(generic.ObjectView): class PlatformView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Platform.objects.all() queryset = Platform.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'),
(VirtualMachine.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -2157,22 +2126,12 @@ class ModuleListView(generic.ObjectListView):
@register_model_view(Module) @register_model_view(Module)
class ModuleView(generic.ObjectView): class ModuleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Module.objects.all() queryset = Module.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Interface.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(ConsolePort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(ConsoleServerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(PowerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(PowerOutlet.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(FrontPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
(RearPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -3552,16 +3511,12 @@ class PowerPanelListView(generic.ObjectListView):
@register_model_view(PowerPanel) @register_model_view(PowerPanel)
class PowerPanelView(generic.ObjectView): class PowerPanelView(GetRelatedModelsMixin, generic.ObjectView):
queryset = PowerPanel.objects.all() queryset = PowerPanel.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(PowerFeed.objects.restrict(request.user).filter(power_panel=instance), 'power_panel_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -3665,16 +3620,18 @@ class VirtualDeviceContextListView(generic.ObjectListView):
@register_model_view(VirtualDeviceContext) @register_model_view(VirtualDeviceContext)
class VirtualDeviceContextView(generic.ObjectView): class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualDeviceContext.objects.all() queryset = VirtualDeviceContext.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(
request,
instance,
extra=(
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
),
),
} }

View File

@ -117,10 +117,14 @@ class BookmarkOrderingChoices(ChoiceSet):
ORDERING_NEWEST = '-created' ORDERING_NEWEST = '-created'
ORDERING_OLDEST = 'created' ORDERING_OLDEST = 'created'
ORDERING_ALPHABETICAL_AZ = 'name'
ORDERING_ALPHABETICAL_ZA = '-name'
CHOICES = ( CHOICES = (
(ORDERING_NEWEST, _('Newest')), (ORDERING_NEWEST, _('Newest')),
(ORDERING_OLDEST, _('Oldest')), (ORDERING_OLDEST, _('Oldest')),
(ORDERING_ALPHABETICAL_AZ, _('Alphabetical (A-Z)')),
(ORDERING_ALPHABETICAL_ZA, _('Alphabetical (Z-A)')),
) )
# #

View File

@ -381,11 +381,17 @@ class BookmarksWidget(DashboardWidget):
if request.user.is_anonymous: if request.user.is_anonymous:
bookmarks = list() bookmarks = list()
else: else:
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by']) user_bookmarks = Bookmark.objects.filter(user=request.user)
if self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_AZ:
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower())
elif self.config['order_by'] == BookmarkOrderingChoices.ORDERING_ALPHABETICAL_ZA:
bookmarks = sorted(user_bookmarks, key=lambda bookmark: bookmark.__str__().lower(), reverse=True)
else:
bookmarks = user_bookmarks.order_by(self.config['order_by'])
if object_types := self.config.get('object_types'): if object_types := self.config.get('object_types'):
models = get_models_from_content_types(object_types) models = get_models_from_content_types(object_types)
conent_types = ObjectType.objects.get_for_models(*models).values() content_types = ObjectType.objects.get_for_models(*models).values()
bookmarks = bookmarks.filter(object_type__in=conent_types) bookmarks = bookmarks.filter(object_type__in=content_types)
if max_items := self.config.get('max_items'): if max_items := self.config.get('max_items'):
bookmarks = bookmarks[:max_items] bookmarks = bookmarks[:max_items]

View File

@ -660,6 +660,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Validate date & time # Validate date & time
elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: elif self.type == CustomFieldTypeChoices.TYPE_DATETIME:
if type(value) is not datetime: if type(value) is not datetime:
# Work around UTC issue for Python < 3.11; see
# https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat
if type(value) is str and value.endswith('Z'):
value = f'{value[:-1]}+00:00'
try: try:
datetime.fromisoformat(value) datetime.fromisoformat(value)
except ValueError: except ValueError:

View File

@ -12,7 +12,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.query import count_related from utilities.query import count_related
from utilities.tables import get_table_ordering from utilities.tables import get_table_ordering
from utilities.views import ViewTab, register_model_view from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface from virtualization.models import VMInterface
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -34,15 +34,10 @@ class VRFListView(generic.ObjectListView):
@register_model_view(VRF) @register_model_view(VRF)
class VRFView(generic.ObjectView): class VRFView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VRF.objects.all() queryset = VRF.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Prefix.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'),
(IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'),
)
import_targets_table = tables.RouteTargetTable( import_targets_table = tables.RouteTargetTable(
instance.import_targets.all(), instance.import_targets.all(),
orderable=False orderable=False
@ -53,7 +48,7 @@ class VRFView(generic.ObjectView):
) )
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]),
'import_targets_table': import_targets_table, 'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table, 'export_targets_table': export_targets_table,
} }
@ -147,16 +142,12 @@ class RIRListView(generic.ObjectListView):
@register_model_view(RIR) @register_model_view(RIR)
class RIRView(generic.ObjectView): class RIRView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RIR.objects.all() queryset = RIR.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Aggregate.objects.restrict(request.user, 'view').filter(rir=instance), 'rir_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -273,17 +264,19 @@ class ASNListView(generic.ObjectListView):
@register_model_view(ASN) @register_model_view(ASN)
class ASNView(generic.ObjectView): class ASNView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ASN.objects.all() queryset = ASN.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = ( return {
'related_models': self.get_related_models(
request,
instance,
extra=(
(Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), (Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
(Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), (Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
) ),
),
return {
'related_models': related_models,
} }
@ -427,18 +420,12 @@ class RoleListView(generic.ObjectListView):
@register_model_view(Role) @register_model_view(Role)
class RoleView(generic.ObjectView): class RoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Role.objects.all() queryset = Role.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Prefix.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
(IPRange.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
(VLAN.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -926,16 +913,12 @@ class VLANGroupListView(generic.ObjectListView):
@register_model_view(VLANGroup) @register_model_view(VLANGroup)
class VLANGroupView(generic.ObjectView): class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -1,3 +1,4 @@
import zoneinfo
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from urllib.parse import quote from urllib.parse import quote
@ -83,6 +84,8 @@ class DateTimeColumn(tables.Column):
def render(self, value): def render(self, value):
if value: if value:
current_tz = zoneinfo.ZoneInfo(settings.TIME_ZONE)
value = value.astimezone(current_tz)
return f"{value.date().isoformat()} {value.time().isoformat(timespec=self.timespec)}" return f"{value.date().isoformat()} {value.time().isoformat(timespec=self.timespec)}"
def value(self, value): def value(self, value):

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,30 @@
import { isTruthy } from '../util';
/**
* Handle saved filter change event.
*
* @param event "change" event for the saved filter select
*/
function handleSavedFilterChange(event: Event): void {
const savedFilter = event.currentTarget as HTMLSelectElement;
let baseUrl = savedFilter.baseURI.split('?')[0];
const preFilter = '?';
const selectedOptions = Array.from(savedFilter.options)
.filter(option => option.selected)
.map(option => `filter_id=${option.value}`)
.join('&');
baseUrl += `${preFilter}${selectedOptions}`;
document.location.href = baseUrl;
}
export function initSavedFilterSelect(): void {
const divResults = document.getElementById('results');
if (isTruthy(divResults)) {
const savedFilterSelect = document.getElementById('id_filter_id');
if (isTruthy(savedFilterSelect)) {
savedFilterSelect.addEventListener('change', handleSavedFilterChange);
}
}
}

View File

@ -13,6 +13,7 @@ import { initSideNav } from './sidenav';
import { initDashboard } from './dashboard'; import { initDashboard } from './dashboard';
import { initRackElevation } from './racks'; import { initRackElevation } from './racks';
import { initHtmx } from './htmx'; import { initHtmx } from './htmx';
import { initSavedFilterSelect } from './forms/savedFiltersSelect';
function initDocument(): void { function initDocument(): void {
for (const init of [ for (const init of [
@ -31,6 +32,7 @@ function initDocument(): void {
initDashboard, initDashboard,
initRackElevation, initRackElevation,
initHtmx, initHtmx,
initSavedFilterSelect,
]) { ]) {
init(); init();
} }

View File

@ -7,6 +7,7 @@
// Overrides of external libraries // Overrides of external libraries
@import 'overrides/bootstrap'; @import 'overrides/bootstrap';
@import 'overrides/tabler'; @import 'overrides/tabler';
@import 'overrides/tomselect';
// Transitional styling to ease migration of templates from NetBox v3.x // Transitional styling to ease migration of templates from NetBox v3.x
@import 'transitional/badges'; @import 'transitional/badges';

View File

@ -0,0 +1,8 @@
.ts-wrapper.multi {
.ts-control {
padding: 7px 7px 3px 7px;
div {
margin: 0 4px 4px 0;
}
}
}

View File

@ -35,6 +35,7 @@ Blocks:
{# User menu (mobile view) #} {# User menu (mobile view) #}
<div class="navbar-nav flex-row d-lg-none"> <div class="navbar-nav flex-row d-lg-none">
{% include 'inc/light_toggle.html' %}
{% include 'inc/user_menu.html' %} {% include 'inc/user_menu.html' %}
</div> </div>
@ -52,14 +53,7 @@ Blocks:
<div class="navbar-nav flex-row align-items-center order-md-last"> <div class="navbar-nav flex-row align-items-center order-md-last">
{# Dark/light mode toggle #} {# Dark/light mode toggle #}
<div class="d-none d-md-flex"> {% include 'inc/light_toggle.html' %}
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb"></i>
</button>
<button class="btn color-mode-toggle hide-theme-light" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb-on"></i>
</button>
</div>
{# User menu #} {# User menu #}
{% include 'inc/user_menu.html' %} {% include 'inc/user_menu.html' %}

View File

@ -28,7 +28,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans "Rack" %}</th> <th scope="row">{% trans "Rack" %}</th>
<td class="d-flex justify-content-between"> <td class="d-flex justify-content-between align-items-start">
{% if object.rack %} {% if object.rack %}
{{ object.rack|linkify }} {{ object.rack|linkify }}
<a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}"> <a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">

View File

@ -73,7 +73,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans "Physical Address" %}</th> <th scope="row">{% trans "Physical Address" %}</th>
<td class="d-flex justify-content-between"> <td class="d-flex justify-content-between align-items-start">
{% if object.physical_address %} {% if object.physical_address %}
<span>{{ object.physical_address|linebreaksbr }}</span> <span>{{ object.physical_address|linebreaksbr }}</span>
{% if config.MAPS_URL %} {% if config.MAPS_URL %}

View File

@ -0,0 +1,10 @@
{% load i18n %}
<div class="d-flex">
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb"></i>
</button>
<button class="btn color-mode-toggle hide-theme-light" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb-on"></i>
</button>
</div>

View File

@ -1,21 +1,32 @@
{% load helpers %} {% load helpers %}
{% load i18n %} {% load i18n %}
<div class="row mb-3"> <div class="row mb-3" id="results">
<div class="col-auto d-print-none"> <div class="col-auto d-print-none">
<div class="input-group input-group-flat me-2 quicksearch" hx-disinherit="hx-select hx-swap"> <div class="input-group input-group-flat me-2 quicksearch" hx-disinherit="hx-select hx-swap">
<input type="search" results="5" name="q" id="quicksearch" class="form-control px-2 py-1" placeholder="Quick search" <input type="search" results="5" name="q" id="quicksearch" class="form-control" placeholder="{% trans "Quick search" %}"
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" /> hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search"/>
<span class="input-group-text py-1"> <span class="input-group-text py-1">
<a href="#" id="quicksearch_clear" class="invisible text-secondary"><i class="mdi mdi-close-circle"></i></a> <a href="#" id="quicksearch_clear" class="invisible text-secondary"><i class="mdi mdi-close-circle"></i></a>
</span> </span>
{% block extra_table_controls %}{% endblock %} {% block extra_table_controls %}{% endblock %}
</div> </div>
</div> </div>
<div class="col-auto d-print-none">
<div class="input-group">
<div class="input-group-text">
<i class="mdi mdi-filter" title="{% trans "Saved filter" %}"></i>
</div>
{{ filter_form.filter_id }}
</div>
</div>
<div class="col-auto ms-auto d-print-none"> <div class="col-auto ms-auto d-print-none">
{% if request.user.is_authenticated and table_modal %} {% if request.user.is_authenticated and table_modal %}
<div class="table-configure input-group"> <div class="table-configure input-group">
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#{{ table_modal }}" <button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}"
data-bs-target="#{{ table_modal }}"
class="btn"> class="btn">
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %} <i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
</button> </button>
@ -23,3 +34,4 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -4,8 +4,7 @@ from django.utils.translation import gettext as _
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.relations import get_related_models from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from utilities.views import register_model_view, ViewTab
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -56,17 +55,14 @@ class TenantGroupListView(generic.ObjectListView):
@register_model_view(TenantGroup) @register_model_view(TenantGroup)
class TenantGroupView(generic.ObjectView): class TenantGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = TenantGroup.objects.all() queryset = TenantGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
related_models = (
(Tenant.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, groups),
} }
@ -123,17 +119,12 @@ class TenantListView(generic.ObjectListView):
@register_model_view(Tenant) @register_model_view(Tenant)
class TenantView(generic.ObjectView): class TenantView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Tenant.objects.all() queryset = Tenant.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = [
(model.objects.restrict(request.user, 'view').filter(tenant=instance), f'{field}_id')
for model, field in get_related_models(Tenant)
]
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -189,17 +180,14 @@ class ContactGroupListView(generic.ObjectListView):
@register_model_view(ContactGroup) @register_model_view(ContactGroup)
class ContactGroupView(generic.ObjectView): class ContactGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ContactGroup.objects.all() queryset = ContactGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
related_models = (
(Contact.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, groups),
} }
@ -256,16 +244,12 @@ class ContactRoleListView(generic.ObjectListView):
@register_model_view(ContactRole) @register_model_view(ContactRole)
class ContactRoleView(generic.ObjectView): class ContactRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ContactRole.objects.all() queryset = ContactRole.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(ContactAssignment.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-08 05:02+0000\n" "POT-Creation-Date: 2024-06-19 05:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -29,7 +29,7 @@ msgid "Write Enabled"
msgstr "" msgstr ""
#: netbox/account/tables.py:35 netbox/core/tables/jobs.py:29 #: netbox/account/tables.py:35 netbox/core/tables/jobs.py:29
#: netbox/core/tables/tasks.py:79 netbox/extras/choices.py:138 #: netbox/core/tables/tasks.py:79 netbox/extras/choices.py:142
#: netbox/extras/tables/tables.py:499 netbox/templates/account/token.html:43 #: netbox/extras/tables/tables.py:499 netbox/templates/account/token.html:43
#: netbox/templates/core/configrevision.html:26 #: netbox/templates/core/configrevision.html:26
#: netbox/templates/core/configrevision_restore.html:12 #: netbox/templates/core/configrevision_restore.html:12
@ -58,7 +58,7 @@ msgstr ""
msgid "Allowed IPs" msgid "Allowed IPs"
msgstr "" msgstr ""
#: netbox/account/views.py:197 #: netbox/account/views.py:204
msgid "Your preferences have been updated." msgid "Your preferences have been updated."
msgstr "" msgstr ""
@ -158,7 +158,7 @@ msgstr ""
#: netbox/circuits/forms/filtersets.py:207 #: netbox/circuits/forms/filtersets.py:207
#: netbox/circuits/forms/model_forms.py:136 #: netbox/circuits/forms/model_forms.py:136
#: netbox/circuits/forms/model_forms.py:152 #: netbox/circuits/forms/model_forms.py:152
#: netbox/circuits/tables/circuits.py:105 netbox/dcim/forms/bulk_edit.py:167 #: netbox/circuits/tables/circuits.py:107 netbox/dcim/forms/bulk_edit.py:167
#: netbox/dcim/forms/bulk_edit.py:239 netbox/dcim/forms/bulk_edit.py:575 #: netbox/dcim/forms/bulk_edit.py:239 netbox/dcim/forms/bulk_edit.py:575
#: netbox/dcim/forms/bulk_edit.py:771 netbox/dcim/forms/bulk_import.py:130 #: netbox/dcim/forms/bulk_edit.py:771 netbox/dcim/forms/bulk_import.py:130
#: netbox/dcim/forms/bulk_import.py:184 netbox/dcim/forms/bulk_import.py:257 #: netbox/dcim/forms/bulk_import.py:184 netbox/dcim/forms/bulk_import.py:257
@ -308,7 +308,7 @@ msgstr ""
#: netbox/circuits/forms/filtersets.py:212 #: netbox/circuits/forms/filtersets.py:212
#: netbox/circuits/forms/model_forms.py:109 #: netbox/circuits/forms/model_forms.py:109
#: netbox/circuits/forms/model_forms.py:131 #: netbox/circuits/forms/model_forms.py:131
#: netbox/circuits/tables/circuits.py:96 netbox/dcim/forms/connections.py:71 #: netbox/circuits/tables/circuits.py:98 netbox/dcim/forms/connections.py:71
#: netbox/templates/circuits/circuit.html:15 #: netbox/templates/circuits/circuit.html:15
#: netbox/templates/circuits/circuittermination.html:19 #: netbox/templates/circuits/circuittermination.html:19
#: netbox/templates/dcim/inc/cable_termination.html:55 #: netbox/templates/dcim/inc/cable_termination.html:55
@ -325,7 +325,7 @@ msgstr ""
#: netbox/circuits/tables/providers.py:33 netbox/dcim/forms/bulk_edit.py:127 #: netbox/circuits/tables/providers.py:33 netbox/dcim/forms/bulk_edit.py:127
#: netbox/dcim/forms/filtersets.py:188 netbox/dcim/forms/model_forms.py:122 #: netbox/dcim/forms/filtersets.py:188 netbox/dcim/forms/model_forms.py:122
#: netbox/dcim/tables/sites.py:94 netbox/ipam/models/asns.py:126 #: netbox/dcim/tables/sites.py:94 netbox/ipam/models/asns.py:126
#: netbox/ipam/tables/asn.py:27 netbox/ipam/views.py:219 #: netbox/ipam/tables/asn.py:27 netbox/ipam/views.py:210
#: netbox/netbox/navigation/menu.py:159 netbox/netbox/navigation/menu.py:162 #: netbox/netbox/navigation/menu.py:159 netbox/netbox/navigation/menu.py:162
#: netbox/templates/circuits/provider.html:23 #: netbox/templates/circuits/provider.html:23
msgid "ASNs" msgid "ASNs"
@ -469,7 +469,7 @@ msgstr ""
#: netbox/circuits/forms/model_forms.py:45 #: netbox/circuits/forms/model_forms.py:45
#: netbox/circuits/forms/model_forms.py:59 #: netbox/circuits/forms/model_forms.py:59
#: netbox/circuits/forms/model_forms.py:91 #: netbox/circuits/forms/model_forms.py:91
#: netbox/circuits/tables/circuits.py:56 netbox/circuits/tables/circuits.py:100 #: netbox/circuits/tables/circuits.py:56 netbox/circuits/tables/circuits.py:102
#: netbox/circuits/tables/providers.py:72 #: netbox/circuits/tables/providers.py:72
#: netbox/circuits/tables/providers.py:103 #: netbox/circuits/tables/providers.py:103
#: netbox/templates/circuits/circuit.html:18 #: netbox/templates/circuits/circuit.html:18
@ -748,7 +748,7 @@ msgstr ""
#: netbox/circuits/forms/bulk_edit.py:191 #: netbox/circuits/forms/bulk_edit.py:191
#: netbox/circuits/forms/bulk_edit.py:215 #: netbox/circuits/forms/bulk_edit.py:215
#: netbox/circuits/forms/model_forms.py:153 #: netbox/circuits/forms/model_forms.py:153
#: netbox/circuits/tables/circuits.py:109 #: netbox/circuits/tables/circuits.py:111
#: netbox/templates/circuits/inc/circuit_termination_fields.html:62 #: netbox/templates/circuits/inc/circuit_termination_fields.html:62
#: netbox/templates/circuits/providernetwork.html:17 #: netbox/templates/circuits/providernetwork.html:17
msgid "Provider Network" msgid "Provider Network"
@ -895,7 +895,7 @@ msgstr ""
#: netbox/dcim/forms/filtersets.py:653 netbox/dcim/forms/filtersets.py:1010 #: netbox/dcim/forms/filtersets.py:653 netbox/dcim/forms/filtersets.py:1010
#: netbox/netbox/navigation/menu.py:44 netbox/netbox/navigation/menu.py:46 #: netbox/netbox/navigation/menu.py:44 netbox/netbox/navigation/menu.py:46
#: netbox/tenancy/forms/filtersets.py:42 netbox/tenancy/tables/columns.py:70 #: netbox/tenancy/forms/filtersets.py:42 netbox/tenancy/tables/columns.py:70
#: netbox/tenancy/tables/contacts.py:25 netbox/tenancy/views.py:19 #: netbox/tenancy/tables/contacts.py:25 netbox/tenancy/views.py:18
#: netbox/virtualization/forms/filtersets.py:37 #: netbox/virtualization/forms/filtersets.py:37
#: netbox/virtualization/forms/filtersets.py:48 #: netbox/virtualization/forms/filtersets.py:48
#: netbox/virtualization/forms/filtersets.py:106 #: netbox/virtualization/forms/filtersets.py:106
@ -1328,21 +1328,21 @@ msgstr ""
msgid "Circuit ID" msgid "Circuit ID"
msgstr "" msgstr ""
#: netbox/circuits/tables/circuits.py:66 #: netbox/circuits/tables/circuits.py:67
#: netbox/wireless/forms/model_forms.py:160 #: netbox/wireless/forms/model_forms.py:160
msgid "Side A" msgid "Side A"
msgstr "" msgstr ""
#: netbox/circuits/tables/circuits.py:70 #: netbox/circuits/tables/circuits.py:72
msgid "Side Z" msgid "Side Z"
msgstr "" msgstr ""
#: netbox/circuits/tables/circuits.py:73 #: netbox/circuits/tables/circuits.py:75
#: netbox/templates/circuits/circuit.html:55 #: netbox/templates/circuits/circuit.html:55
msgid "Commit Rate" msgid "Commit Rate"
msgstr "" msgstr ""
#: netbox/circuits/tables/circuits.py:76 netbox/circuits/tables/providers.py:48 #: netbox/circuits/tables/circuits.py:78 netbox/circuits/tables/providers.py:48
#: netbox/circuits/tables/providers.py:82 #: netbox/circuits/tables/providers.py:82
#: netbox/circuits/tables/providers.py:107 netbox/dcim/tables/devices.py:1001 #: netbox/circuits/tables/providers.py:107 netbox/dcim/tables/devices.py:1001
#: netbox/dcim/tables/devicetypes.py:92 netbox/dcim/tables/modules.py:29 #: netbox/dcim/tables/devicetypes.py:92 netbox/dcim/tables/modules.py:29
@ -1400,7 +1400,7 @@ msgid "Syncing"
msgstr "" msgstr ""
#: netbox/core/choices.py:21 netbox/core/choices.py:57 #: netbox/core/choices.py:21 netbox/core/choices.py:57
#: netbox/core/tables/jobs.py:41 netbox/extras/choices.py:224 #: netbox/core/tables/jobs.py:41 netbox/extras/choices.py:228
#: netbox/templates/core/job.html:68 #: netbox/templates/core/job.html:68
msgid "Completed" msgid "Completed"
msgstr "" msgstr ""
@ -1408,7 +1408,7 @@ msgstr ""
#: netbox/core/choices.py:22 netbox/core/choices.py:59 #: netbox/core/choices.py:22 netbox/core/choices.py:59
#: netbox/core/constants.py:20 netbox/core/tables/tasks.py:34 #: netbox/core/constants.py:20 netbox/core/tables/tasks.py:34
#: netbox/dcim/choices.py:176 netbox/dcim/choices.py:222 #: netbox/dcim/choices.py:176 netbox/dcim/choices.py:222
#: netbox/dcim/choices.py:1536 netbox/extras/choices.py:226 #: netbox/dcim/choices.py:1536 netbox/extras/choices.py:230
#: netbox/virtualization/choices.py:47 #: netbox/virtualization/choices.py:47
msgid "Failed" msgid "Failed"
msgstr "" msgstr ""
@ -1426,21 +1426,21 @@ msgstr ""
msgid "Reports" msgid "Reports"
msgstr "" msgstr ""
#: netbox/core/choices.py:54 netbox/extras/choices.py:221 #: netbox/core/choices.py:54 netbox/extras/choices.py:225
msgid "Pending" msgid "Pending"
msgstr "" msgstr ""
#: netbox/core/choices.py:55 netbox/core/constants.py:23 #: netbox/core/choices.py:55 netbox/core/constants.py:23
#: netbox/core/tables/jobs.py:32 netbox/core/tables/tasks.py:38 #: netbox/core/tables/jobs.py:32 netbox/core/tables/tasks.py:38
#: netbox/extras/choices.py:222 netbox/templates/core/job.html:55 #: netbox/extras/choices.py:226 netbox/templates/core/job.html:55
msgid "Scheduled" msgid "Scheduled"
msgstr "" msgstr ""
#: netbox/core/choices.py:56 netbox/extras/choices.py:223 #: netbox/core/choices.py:56 netbox/extras/choices.py:227
msgid "Running" msgid "Running"
msgstr "" msgstr ""
#: netbox/core/choices.py:58 netbox/extras/choices.py:225 #: netbox/core/choices.py:58 netbox/extras/choices.py:229
msgid "Errored" msgid "Errored"
msgstr "" msgstr ""
@ -2067,8 +2067,8 @@ msgstr ""
msgid "No workers found" msgid "No workers found"
msgstr "" msgstr ""
#: netbox/core/views.py:335 netbox/core/views.py:378 netbox/core/views.py:401 #: netbox/core/views.py:331 netbox/core/views.py:374 netbox/core/views.py:397
#: netbox/core/views.py:419 netbox/core/views.py:454 #: netbox/core/views.py:415 netbox/core/views.py:450
#, python-brace-format #, python-brace-format
msgid "Job {job_id} not found" msgid "Job {job_id} not found"
msgstr "" msgstr ""
@ -2946,7 +2946,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_create.py:40 netbox/extras/forms/filtersets.py:410 #: netbox/dcim/forms/bulk_create.py:40 netbox/extras/forms/filtersets.py:410
#: netbox/extras/forms/model_forms.py:443 #: netbox/extras/forms/model_forms.py:443
#: netbox/extras/forms/model_forms.py:495 netbox/netbox/forms/base.py:84 #: netbox/extras/forms/model_forms.py:495 netbox/netbox/forms/base.py:84
#: netbox/netbox/forms/mixins.py:81 netbox/netbox/tables/columns.py:458 #: netbox/netbox/forms/mixins.py:81 netbox/netbox/tables/columns.py:461
#: netbox/templates/circuits/inc/circuit_termination.html:32 #: netbox/templates/circuits/inc/circuit_termination.html:32
#: netbox/templates/generic/bulk_edit.html:65 #: netbox/templates/generic/bulk_edit.html:65
#: netbox/templates/inc/panels/tags.html:5 #: netbox/templates/inc/panels/tags.html:5
@ -5974,7 +5974,7 @@ msgstr ""
#: netbox/netbox/navigation/menu.py:60 netbox/netbox/navigation/menu.py:62 #: netbox/netbox/navigation/menu.py:60 netbox/netbox/navigation/menu.py:62
#: netbox/virtualization/forms/model_forms.py:122 #: netbox/virtualization/forms/model_forms.py:122
#: netbox/virtualization/tables/clusters.py:83 #: netbox/virtualization/tables/clusters.py:83
#: netbox/virtualization/views.py:210 #: netbox/virtualization/views.py:202
msgid "Devices" msgid "Devices"
msgstr "" msgstr ""
@ -6054,8 +6054,8 @@ msgid "Power outlets"
msgstr "" msgstr ""
#: netbox/dcim/tables/devices.py:243 netbox/dcim/tables/devices.py:1046 #: netbox/dcim/tables/devices.py:243 netbox/dcim/tables/devices.py:1046
#: netbox/dcim/tables/devicetypes.py:125 netbox/dcim/views.py:1006 #: netbox/dcim/tables/devicetypes.py:125 netbox/dcim/views.py:985
#: netbox/dcim/views.py:1245 netbox/dcim/views.py:1931 #: netbox/dcim/views.py:1224 netbox/dcim/views.py:1900
#: netbox/netbox/navigation/menu.py:81 netbox/netbox/navigation/menu.py:237 #: netbox/netbox/navigation/menu.py:81 netbox/netbox/navigation/menu.py:237
#: netbox/templates/dcim/device/base.html:37 #: netbox/templates/dcim/device/base.html:37
#: netbox/templates/dcim/device_list.html:43 #: netbox/templates/dcim/device_list.html:43
@ -6067,7 +6067,7 @@ msgstr ""
#: netbox/templates/virtualization/virtualmachine/base.html:27 #: netbox/templates/virtualization/virtualmachine/base.html:27
#: netbox/templates/virtualization/virtualmachine_list.html:14 #: netbox/templates/virtualization/virtualmachine_list.html:14
#: netbox/virtualization/tables/virtualmachines.py:100 #: netbox/virtualization/tables/virtualmachines.py:100
#: netbox/virtualization/views.py:367 netbox/wireless/tables/wirelesslan.py:55 #: netbox/virtualization/views.py:359 netbox/wireless/tables/wirelesslan.py:55
msgid "Interfaces" msgid "Interfaces"
msgstr "" msgstr ""
@ -6093,8 +6093,8 @@ msgid "Module Bay"
msgstr "" msgstr ""
#: netbox/dcim/tables/devices.py:310 netbox/dcim/tables/devicetypes.py:48 #: netbox/dcim/tables/devices.py:310 netbox/dcim/tables/devicetypes.py:48
#: netbox/dcim/tables/devicetypes.py:140 netbox/dcim/views.py:1081 #: netbox/dcim/tables/devicetypes.py:140 netbox/dcim/views.py:1060
#: netbox/dcim/views.py:2024 netbox/netbox/navigation/menu.py:90 #: netbox/dcim/views.py:1993 netbox/netbox/navigation/menu.py:90
#: netbox/templates/dcim/device/base.html:52 #: netbox/templates/dcim/device/base.html:52
#: netbox/templates/dcim/device_list.html:71 #: netbox/templates/dcim/device_list.html:71
#: netbox/templates/dcim/devicetype/base.html:49 #: netbox/templates/dcim/devicetype/base.html:49
@ -6124,8 +6124,8 @@ msgid "Allocated draw (W)"
msgstr "" msgstr ""
#: netbox/dcim/tables/devices.py:546 netbox/ipam/forms/model_forms.py:747 #: netbox/dcim/tables/devices.py:546 netbox/ipam/forms/model_forms.py:747
#: netbox/ipam/tables/fhrp.py:28 netbox/ipam/views.py:602 #: netbox/ipam/tables/fhrp.py:28 netbox/ipam/views.py:589
#: netbox/ipam/views.py:701 netbox/netbox/navigation/menu.py:145 #: netbox/ipam/views.py:688 netbox/netbox/navigation/menu.py:145
#: netbox/netbox/navigation/menu.py:147 #: netbox/netbox/navigation/menu.py:147
#: netbox/templates/dcim/interface.html:339 #: netbox/templates/dcim/interface.html:339
#: netbox/templates/ipam/ipaddress_bulk_add.html:15 #: netbox/templates/ipam/ipaddress_bulk_add.html:15
@ -6218,8 +6218,8 @@ msgstr ""
msgid "Instances" msgid "Instances"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:113 netbox/dcim/views.py:946 #: netbox/dcim/tables/devicetypes.py:113 netbox/dcim/views.py:925
#: netbox/dcim/views.py:1185 netbox/dcim/views.py:1871 #: netbox/dcim/views.py:1164 netbox/dcim/views.py:1840
#: netbox/netbox/navigation/menu.py:84 #: netbox/netbox/navigation/menu.py:84
#: netbox/templates/dcim/device/base.html:25 #: netbox/templates/dcim/device/base.html:25
#: netbox/templates/dcim/device_list.html:15 #: netbox/templates/dcim/device_list.html:15
@ -6229,8 +6229,8 @@ msgstr ""
msgid "Console Ports" msgid "Console Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:116 netbox/dcim/views.py:961 #: netbox/dcim/tables/devicetypes.py:116 netbox/dcim/views.py:940
#: netbox/dcim/views.py:1200 netbox/dcim/views.py:1886 #: netbox/dcim/views.py:1179 netbox/dcim/views.py:1855
#: netbox/netbox/navigation/menu.py:85 #: netbox/netbox/navigation/menu.py:85
#: netbox/templates/dcim/device/base.html:28 #: netbox/templates/dcim/device/base.html:28
#: netbox/templates/dcim/device_list.html:22 #: netbox/templates/dcim/device_list.html:22
@ -6240,8 +6240,8 @@ msgstr ""
msgid "Console Server Ports" msgid "Console Server Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:119 netbox/dcim/views.py:976 #: netbox/dcim/tables/devicetypes.py:119 netbox/dcim/views.py:955
#: netbox/dcim/views.py:1215 netbox/dcim/views.py:1901 #: netbox/dcim/views.py:1194 netbox/dcim/views.py:1870
#: netbox/netbox/navigation/menu.py:86 #: netbox/netbox/navigation/menu.py:86
#: netbox/templates/dcim/device/base.html:31 #: netbox/templates/dcim/device/base.html:31
#: netbox/templates/dcim/device_list.html:29 #: netbox/templates/dcim/device_list.html:29
@ -6251,8 +6251,8 @@ msgstr ""
msgid "Power Ports" msgid "Power Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:122 netbox/dcim/views.py:991 #: netbox/dcim/tables/devicetypes.py:122 netbox/dcim/views.py:970
#: netbox/dcim/views.py:1230 netbox/dcim/views.py:1916 #: netbox/dcim/views.py:1209 netbox/dcim/views.py:1885
#: netbox/netbox/navigation/menu.py:87 #: netbox/netbox/navigation/menu.py:87
#: netbox/templates/dcim/device/base.html:34 #: netbox/templates/dcim/device/base.html:34
#: netbox/templates/dcim/device_list.html:36 #: netbox/templates/dcim/device_list.html:36
@ -6262,8 +6262,8 @@ msgstr ""
msgid "Power Outlets" msgid "Power Outlets"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:128 netbox/dcim/views.py:1021 #: netbox/dcim/tables/devicetypes.py:128 netbox/dcim/views.py:1000
#: netbox/dcim/views.py:1260 netbox/dcim/views.py:1952 #: netbox/dcim/views.py:1239 netbox/dcim/views.py:1921
#: netbox/netbox/navigation/menu.py:82 #: netbox/netbox/navigation/menu.py:82
#: netbox/templates/dcim/device/base.html:40 #: netbox/templates/dcim/device/base.html:40
#: netbox/templates/dcim/devicetype/base.html:37 #: netbox/templates/dcim/devicetype/base.html:37
@ -6272,8 +6272,8 @@ msgstr ""
msgid "Front Ports" msgid "Front Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:131 netbox/dcim/views.py:1036 #: netbox/dcim/tables/devicetypes.py:131 netbox/dcim/views.py:1015
#: netbox/dcim/views.py:1275 netbox/dcim/views.py:1967 #: netbox/dcim/views.py:1254 netbox/dcim/views.py:1936
#: netbox/netbox/navigation/menu.py:83 #: netbox/netbox/navigation/menu.py:83
#: netbox/templates/dcim/device/base.html:43 #: netbox/templates/dcim/device/base.html:43
#: netbox/templates/dcim/device_list.html:50 #: netbox/templates/dcim/device_list.html:50
@ -6283,16 +6283,16 @@ msgstr ""
msgid "Rear Ports" msgid "Rear Ports"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:134 netbox/dcim/views.py:1066 #: netbox/dcim/tables/devicetypes.py:134 netbox/dcim/views.py:1045
#: netbox/dcim/views.py:2005 netbox/netbox/navigation/menu.py:89 #: netbox/dcim/views.py:1974 netbox/netbox/navigation/menu.py:89
#: netbox/templates/dcim/device/base.html:49 #: netbox/templates/dcim/device/base.html:49
#: netbox/templates/dcim/device_list.html:57 #: netbox/templates/dcim/device_list.html:57
#: netbox/templates/dcim/devicetype/base.html:46 #: netbox/templates/dcim/devicetype/base.html:46
msgid "Device Bays" msgid "Device Bays"
msgstr "" msgstr ""
#: netbox/dcim/tables/devicetypes.py:137 netbox/dcim/views.py:1051 #: netbox/dcim/tables/devicetypes.py:137 netbox/dcim/views.py:1030
#: netbox/dcim/views.py:1986 netbox/netbox/navigation/menu.py:88 #: netbox/dcim/views.py:1955 netbox/netbox/navigation/menu.py:88
#: netbox/templates/dcim/device/base.html:46 #: netbox/templates/dcim/device/base.html:46
#: netbox/templates/dcim/device_list.html:64 #: netbox/templates/dcim/device_list.html:64
#: netbox/templates/dcim/devicetype/base.html:43 #: netbox/templates/dcim/devicetype/base.html:43
@ -6350,38 +6350,38 @@ msgstr ""
msgid "Test case must set peer_termination_type" msgid "Test case must set peer_termination_type"
msgstr "" msgstr ""
#: netbox/dcim/views.py:137 #: netbox/dcim/views.py:139
#, python-brace-format #, python-brace-format
msgid "Disconnected {count} {type}" msgid "Disconnected {count} {type}"
msgstr "" msgstr ""
#: netbox/dcim/views.py:698 netbox/netbox/navigation/menu.py:28 #: netbox/dcim/views.py:684 netbox/netbox/navigation/menu.py:28
msgid "Reservations" msgid "Reservations"
msgstr "" msgstr ""
#: netbox/dcim/views.py:716 netbox/templates/dcim/location.html:90 #: netbox/dcim/views.py:702 netbox/templates/dcim/location.html:90
#: netbox/templates/dcim/site.html:140 #: netbox/templates/dcim/site.html:140
msgid "Non-Racked Devices" msgid "Non-Racked Devices"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2037 netbox/extras/forms/model_forms.py:453 #: netbox/dcim/views.py:2006 netbox/extras/forms/model_forms.py:453
#: netbox/templates/extras/configcontext.html:10 #: netbox/templates/extras/configcontext.html:10
#: netbox/virtualization/forms/model_forms.py:225 #: netbox/virtualization/forms/model_forms.py:225
#: netbox/virtualization/views.py:407 #: netbox/virtualization/views.py:399
msgid "Config Context" msgid "Config Context"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2047 netbox/virtualization/views.py:417 #: netbox/dcim/views.py:2016 netbox/virtualization/views.py:409
msgid "Render Config" msgid "Render Config"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2097 netbox/extras/tables/tables.py:440 #: netbox/dcim/views.py:2066 netbox/extras/tables/tables.py:440
#: netbox/netbox/navigation/menu.py:234 netbox/netbox/navigation/menu.py:236 #: netbox/netbox/navigation/menu.py:234 netbox/netbox/navigation/menu.py:236
#: netbox/virtualization/views.py:185 #: netbox/virtualization/views.py:177
msgid "Virtual Machines" msgid "Virtual Machines"
msgstr "" msgstr ""
#: netbox/dcim/views.py:2989 netbox/ipam/tables/ip.py:233 #: netbox/dcim/views.py:2948 netbox/ipam/tables/ip.py:233
msgid "Children" msgid "Children"
msgstr "" msgstr ""
@ -6483,71 +6483,79 @@ msgstr ""
msgid "Link" msgid "Link"
msgstr "" msgstr ""
#: netbox/extras/choices.py:122 #: netbox/extras/choices.py:124
msgid "Newest" msgid "Newest"
msgstr "" msgstr ""
#: netbox/extras/choices.py:123 #: netbox/extras/choices.py:125
msgid "Oldest" msgid "Oldest"
msgstr "" msgstr ""
#: netbox/extras/choices.py:139 netbox/templates/generic/object.html:61 #: netbox/extras/choices.py:126
msgid "Alphabetical (A-Z)"
msgstr ""
#: netbox/extras/choices.py:127
msgid "Alphabetical (Z-A)"
msgstr ""
#: netbox/extras/choices.py:143 netbox/templates/generic/object.html:61
msgid "Updated" msgid "Updated"
msgstr "" msgstr ""
#: netbox/extras/choices.py:140 #: netbox/extras/choices.py:144
msgid "Deleted" msgid "Deleted"
msgstr "" msgstr ""
#: netbox/extras/choices.py:157 netbox/extras/choices.py:181 #: netbox/extras/choices.py:161 netbox/extras/choices.py:185
msgid "Info" msgid "Info"
msgstr "" msgstr ""
#: netbox/extras/choices.py:158 netbox/extras/choices.py:180 #: netbox/extras/choices.py:162 netbox/extras/choices.py:184
msgid "Success" msgid "Success"
msgstr "" msgstr ""
#: netbox/extras/choices.py:159 netbox/extras/choices.py:182 #: netbox/extras/choices.py:163 netbox/extras/choices.py:186
msgid "Warning" msgid "Warning"
msgstr "" msgstr ""
#: netbox/extras/choices.py:160 #: netbox/extras/choices.py:164
msgid "Danger" msgid "Danger"
msgstr "" msgstr ""
#: netbox/extras/choices.py:178 #: netbox/extras/choices.py:182
msgid "Debug" msgid "Debug"
msgstr "" msgstr ""
#: netbox/extras/choices.py:179 netbox/netbox/choices.py:104 #: netbox/extras/choices.py:183 netbox/netbox/choices.py:104
msgid "Default" msgid "Default"
msgstr "" msgstr ""
#: netbox/extras/choices.py:183 #: netbox/extras/choices.py:187
msgid "Failure" msgid "Failure"
msgstr "" msgstr ""
#: netbox/extras/choices.py:199 #: netbox/extras/choices.py:203
msgid "Hourly" msgid "Hourly"
msgstr "" msgstr ""
#: netbox/extras/choices.py:200 #: netbox/extras/choices.py:204
msgid "12 hours" msgid "12 hours"
msgstr "" msgstr ""
#: netbox/extras/choices.py:201 #: netbox/extras/choices.py:205
msgid "Daily" msgid "Daily"
msgstr "" msgstr ""
#: netbox/extras/choices.py:202 #: netbox/extras/choices.py:206
msgid "Weekly" msgid "Weekly"
msgstr "" msgstr ""
#: netbox/extras/choices.py:203 #: netbox/extras/choices.py:207
msgid "30 days" msgid "30 days"
msgstr "" msgstr ""
#: netbox/extras/choices.py:268 netbox/extras/tables/tables.py:296 #: netbox/extras/choices.py:272 netbox/extras/tables/tables.py:296
#: netbox/templates/dcim/virtualchassis_edit.html:107 #: netbox/templates/dcim/virtualchassis_edit.html:107
#: netbox/templates/extras/eventrule.html:40 #: netbox/templates/extras/eventrule.html:40
#: netbox/templates/generic/bulk_add_component.html:68 #: netbox/templates/generic/bulk_add_component.html:68
@ -6557,12 +6565,12 @@ msgstr ""
msgid "Create" msgid "Create"
msgstr "" msgstr ""
#: netbox/extras/choices.py:269 netbox/extras/tables/tables.py:299 #: netbox/extras/choices.py:273 netbox/extras/tables/tables.py:299
#: netbox/templates/extras/eventrule.html:44 #: netbox/templates/extras/eventrule.html:44
msgid "Update" msgid "Update"
msgstr "" msgstr ""
#: netbox/extras/choices.py:270 netbox/extras/tables/tables.py:302 #: netbox/extras/choices.py:274 netbox/extras/tables/tables.py:302
#: netbox/templates/circuits/inc/circuit_termination.html:23 #: netbox/templates/circuits/inc/circuit_termination.html:23
#: netbox/templates/dcim/inc/panels/inventory_items.html:37 #: netbox/templates/dcim/inc/panels/inventory_items.html:37
#: netbox/templates/dcim/moduletype/component_templates.html:23 #: netbox/templates/dcim/moduletype/component_templates.html:23
@ -6579,77 +6587,77 @@ msgstr ""
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: netbox/extras/choices.py:294 netbox/netbox/choices.py:57 #: netbox/extras/choices.py:298 netbox/netbox/choices.py:57
#: netbox/netbox/choices.py:105 #: netbox/netbox/choices.py:105
msgid "Blue" msgid "Blue"
msgstr "" msgstr ""
#: netbox/extras/choices.py:295 netbox/netbox/choices.py:56 #: netbox/extras/choices.py:299 netbox/netbox/choices.py:56
#: netbox/netbox/choices.py:106 #: netbox/netbox/choices.py:106
msgid "Indigo" msgid "Indigo"
msgstr "" msgstr ""
#: netbox/extras/choices.py:296 netbox/netbox/choices.py:54 #: netbox/extras/choices.py:300 netbox/netbox/choices.py:54
#: netbox/netbox/choices.py:107 #: netbox/netbox/choices.py:107
msgid "Purple" msgid "Purple"
msgstr "" msgstr ""
#: netbox/extras/choices.py:297 netbox/netbox/choices.py:51 #: netbox/extras/choices.py:301 netbox/netbox/choices.py:51
#: netbox/netbox/choices.py:108 #: netbox/netbox/choices.py:108
msgid "Pink" msgid "Pink"
msgstr "" msgstr ""
#: netbox/extras/choices.py:298 netbox/netbox/choices.py:50 #: netbox/extras/choices.py:302 netbox/netbox/choices.py:50
#: netbox/netbox/choices.py:109 #: netbox/netbox/choices.py:109
msgid "Red" msgid "Red"
msgstr "" msgstr ""
#: netbox/extras/choices.py:299 netbox/netbox/choices.py:68 #: netbox/extras/choices.py:303 netbox/netbox/choices.py:68
#: netbox/netbox/choices.py:110 #: netbox/netbox/choices.py:110
msgid "Orange" msgid "Orange"
msgstr "" msgstr ""
#: netbox/extras/choices.py:300 netbox/netbox/choices.py:66 #: netbox/extras/choices.py:304 netbox/netbox/choices.py:66
#: netbox/netbox/choices.py:111 #: netbox/netbox/choices.py:111
msgid "Yellow" msgid "Yellow"
msgstr "" msgstr ""
#: netbox/extras/choices.py:301 netbox/netbox/choices.py:63 #: netbox/extras/choices.py:305 netbox/netbox/choices.py:63
#: netbox/netbox/choices.py:112 #: netbox/netbox/choices.py:112
msgid "Green" msgid "Green"
msgstr "" msgstr ""
#: netbox/extras/choices.py:302 netbox/netbox/choices.py:60 #: netbox/extras/choices.py:306 netbox/netbox/choices.py:60
#: netbox/netbox/choices.py:113 #: netbox/netbox/choices.py:113
msgid "Teal" msgid "Teal"
msgstr "" msgstr ""
#: netbox/extras/choices.py:303 netbox/netbox/choices.py:59 #: netbox/extras/choices.py:307 netbox/netbox/choices.py:59
#: netbox/netbox/choices.py:114 #: netbox/netbox/choices.py:114
msgid "Cyan" msgid "Cyan"
msgstr "" msgstr ""
#: netbox/extras/choices.py:304 netbox/netbox/choices.py:115 #: netbox/extras/choices.py:308 netbox/netbox/choices.py:115
msgid "Gray" msgid "Gray"
msgstr "" msgstr ""
#: netbox/extras/choices.py:305 netbox/netbox/choices.py:74 #: netbox/extras/choices.py:309 netbox/netbox/choices.py:74
#: netbox/netbox/choices.py:116 #: netbox/netbox/choices.py:116
msgid "Black" msgid "Black"
msgstr "" msgstr ""
#: netbox/extras/choices.py:306 netbox/netbox/choices.py:75 #: netbox/extras/choices.py:310 netbox/netbox/choices.py:75
#: netbox/netbox/choices.py:117 #: netbox/netbox/choices.py:117
msgid "White" msgid "White"
msgstr "" msgstr ""
#: netbox/extras/choices.py:320 netbox/extras/forms/model_forms.py:242 #: netbox/extras/choices.py:324 netbox/extras/forms/model_forms.py:242
#: netbox/extras/forms/model_forms.py:324 #: netbox/extras/forms/model_forms.py:324
#: netbox/templates/extras/webhook.html:10 #: netbox/templates/extras/webhook.html:10
msgid "Webhook" msgid "Webhook"
msgstr "" msgstr ""
#: netbox/extras/choices.py:321 netbox/extras/forms/model_forms.py:312 #: netbox/extras/choices.py:325 netbox/extras/forms/model_forms.py:312
#: netbox/templates/extras/script/base.html:29 #: netbox/templates/extras/script/base.html:29
msgid "Script" msgid "Script"
msgstr "" msgstr ""
@ -7678,56 +7686,56 @@ msgstr ""
msgid "Date values must be in ISO 8601 format (YYYY-MM-DD)." msgid "Date values must be in ISO 8601 format (YYYY-MM-DD)."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:667 #: netbox/extras/models/customfields.py:671
msgid "Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)." msgid "Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:674 #: netbox/extras/models/customfields.py:678
#, python-brace-format #, python-brace-format
msgid "Invalid choice ({value}) for choice set {choiceset}." msgid "Invalid choice ({value}) for choice set {choiceset}."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:684 #: netbox/extras/models/customfields.py:688
#, python-brace-format #, python-brace-format
msgid "Invalid choice(s) ({value}) for choice set {choiceset}." msgid "Invalid choice(s) ({value}) for choice set {choiceset}."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:693 #: netbox/extras/models/customfields.py:697
#, python-brace-format #, python-brace-format
msgid "Value must be an object ID, not {type}" msgid "Value must be an object ID, not {type}"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:699 #: netbox/extras/models/customfields.py:703
#, python-brace-format #, python-brace-format
msgid "Value must be a list of object IDs, not {type}" msgid "Value must be a list of object IDs, not {type}"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:703 #: netbox/extras/models/customfields.py:707
#, python-brace-format #, python-brace-format
msgid "Found invalid object ID: {id}" msgid "Found invalid object ID: {id}"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:706 #: netbox/extras/models/customfields.py:710
msgid "Required field cannot be empty." msgid "Required field cannot be empty."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:725 #: netbox/extras/models/customfields.py:729
msgid "Base set of predefined choices (optional)" msgid "Base set of predefined choices (optional)"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:737 #: netbox/extras/models/customfields.py:741
msgid "Choices are automatically ordered alphabetically" msgid "Choices are automatically ordered alphabetically"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:744 #: netbox/extras/models/customfields.py:748
msgid "custom field choice set" msgid "custom field choice set"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:745 #: netbox/extras/models/customfields.py:749
msgid "custom field choice sets" msgid "custom field choice sets"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:781 #: netbox/extras/models/customfields.py:785
msgid "Must define base or extra choices." msgid "Must define base or extra choices."
msgstr "" msgstr ""
@ -9407,7 +9415,7 @@ msgid "The primary function of this VLAN"
msgstr "" msgstr ""
#: netbox/ipam/models/vlans.py:215 netbox/ipam/tables/ip.py:175 #: netbox/ipam/models/vlans.py:215 netbox/ipam/tables/ip.py:175
#: netbox/ipam/tables/vlans.py:78 netbox/ipam/views.py:978 #: netbox/ipam/tables/vlans.py:78 netbox/ipam/views.py:961
#: netbox/netbox/navigation/menu.py:180 netbox/netbox/navigation/menu.py:182 #: netbox/netbox/navigation/menu.py:180 netbox/netbox/navigation/menu.py:182
msgid "VLANs" msgid "VLANs"
msgstr "" msgstr ""
@ -9479,7 +9487,7 @@ msgid "Added"
msgstr "" msgstr ""
#: netbox/ipam/tables/ip.py:127 netbox/ipam/tables/ip.py:165 #: netbox/ipam/tables/ip.py:127 netbox/ipam/tables/ip.py:165
#: netbox/ipam/tables/vlans.py:138 netbox/ipam/views.py:349 #: netbox/ipam/tables/vlans.py:138 netbox/ipam/views.py:342
#: netbox/netbox/navigation/menu.py:152 netbox/netbox/navigation/menu.py:154 #: netbox/netbox/navigation/menu.py:152 netbox/netbox/navigation/menu.py:154
#: netbox/templates/ipam/vlan.html:84 #: netbox/templates/ipam/vlan.html:84
msgid "Prefixes" msgid "Prefixes"
@ -9580,23 +9588,23 @@ msgid ""
"are allowed in DNS names" "are allowed in DNS names"
msgstr "" msgstr ""
#: netbox/ipam/views.py:541 #: netbox/ipam/views.py:528
msgid "Child Prefixes" msgid "Child Prefixes"
msgstr "" msgstr ""
#: netbox/ipam/views.py:576 #: netbox/ipam/views.py:563
msgid "Child Ranges" msgid "Child Ranges"
msgstr "" msgstr ""
#: netbox/ipam/views.py:902 #: netbox/ipam/views.py:889
msgid "Related IPs" msgid "Related IPs"
msgstr "" msgstr ""
#: netbox/ipam/views.py:1133 #: netbox/ipam/views.py:1116
msgid "Device Interfaces" msgid "Device Interfaces"
msgstr "" msgstr ""
#: netbox/ipam/views.py:1150 #: netbox/ipam/views.py:1133
msgid "VM Interfaces" msgid "VM Interfaces"
msgstr "" msgstr ""
@ -10151,7 +10159,7 @@ msgstr ""
#: netbox/templates/virtualization/virtualmachine/base.html:32 #: netbox/templates/virtualization/virtualmachine/base.html:32
#: netbox/templates/virtualization/virtualmachine_list.html:21 #: netbox/templates/virtualization/virtualmachine_list.html:21
#: netbox/virtualization/tables/virtualmachines.py:103 #: netbox/virtualization/tables/virtualmachines.py:103
#: netbox/virtualization/views.py:388 #: netbox/virtualization/views.py:380
msgid "Virtual Disks" msgid "Virtual Disks"
msgstr "" msgstr ""
@ -10490,15 +10498,15 @@ msgstr ""
msgid "Chinese" msgid "Chinese"
msgstr "" msgstr ""
#: netbox/netbox/tables/columns.py:185 #: netbox/netbox/tables/columns.py:188
msgid "Toggle all" msgid "Toggle all"
msgstr "" msgstr ""
#: netbox/netbox/tables/columns.py:287 #: netbox/netbox/tables/columns.py:290
msgid "Toggle Dropdown" msgid "Toggle Dropdown"
msgstr "" msgstr ""
#: netbox/netbox/tables/columns.py:552 netbox/templates/core/job.html:35 #: netbox/netbox/tables/columns.py:555 netbox/templates/core/job.html:35
msgid "Error" msgid "Error"
msgstr "" msgstr ""
@ -10782,36 +10790,28 @@ msgstr ""
msgid "NetBox Logo" msgid "NetBox Logo"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:56 #: netbox/templates/base/layout.html:139
msgid "Enable dark mode"
msgstr ""
#: netbox/templates/base/layout.html:59
msgid "Enable light mode"
msgstr ""
#: netbox/templates/base/layout.html:145
msgid "Docs" msgid "Docs"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:151 #: netbox/templates/base/layout.html:145
#: netbox/templates/rest_framework/api.html:10 #: netbox/templates/rest_framework/api.html:10
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:157 #: netbox/templates/base/layout.html:151
msgid "REST API documentation" msgid "REST API documentation"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:164 #: netbox/templates/base/layout.html:158
msgid "GraphQL API" msgid "GraphQL API"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:171 #: netbox/templates/base/layout.html:165
msgid "Source Code" msgid "Source Code"
msgstr "" msgstr ""
#: netbox/templates/base/layout.html:177 #: netbox/templates/base/layout.html:171
msgid "Community" msgid "Community"
msgstr "" msgstr ""
@ -11104,8 +11104,8 @@ msgstr ""
#: netbox/templates/core/rq_worker_list.html:45 #: netbox/templates/core/rq_worker_list.html:45
#: netbox/templates/extras/script_result.html:49 #: netbox/templates/extras/script_result.html:49
#: netbox/templates/extras/script_result.html:51 #: netbox/templates/extras/script_result.html:51
#: netbox/templates/inc/table_controls_htmx.html:18 #: netbox/templates/inc/table_controls_htmx.html:28
#: netbox/templates/inc/table_controls_htmx.html:20 #: netbox/templates/inc/table_controls_htmx.html:31
msgid "Configure Table" msgid "Configure Table"
msgstr "" msgstr ""
@ -12681,6 +12681,14 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
#: netbox/templates/inc/light_toggle.html:4
msgid "Enable dark mode"
msgstr ""
#: netbox/templates/inc/light_toggle.html:7
msgid "Enable light mode"
msgstr ""
#: netbox/templates/inc/missing_prerequisites.html:8 #: netbox/templates/inc/missing_prerequisites.html:8
#, python-format #, python-format
msgid "" msgid ""
@ -12721,6 +12729,14 @@ msgstr ""
msgid "Data is out of sync with upstream file" msgid "Data is out of sync with upstream file"
msgstr "" msgstr ""
#: netbox/templates/inc/table_controls_htmx.html:7
msgid "Quick search"
msgstr ""
#: netbox/templates/inc/table_controls_htmx.html:19
msgid "Saved filter"
msgstr ""
#: netbox/templates/inc/user_menu.html:23 #: netbox/templates/inc/user_menu.html:23
msgid "Django Admin" msgid "Django Admin"
msgstr "" msgstr ""
@ -14064,17 +14080,17 @@ msgstr ""
msgid "{value} is not a valid regular expression." msgid "{value} is not a valid regular expression."
msgstr "" msgstr ""
#: netbox/utilities/views.py:40 #: netbox/utilities/views.py:44
#, python-brace-format #, python-brace-format
msgid "{self.__class__.__name__} must implement get_required_permission()" msgid "{self.__class__.__name__} must implement get_required_permission()"
msgstr "" msgstr ""
#: netbox/utilities/views.py:76 #: netbox/utilities/views.py:80
#, python-brace-format #, python-brace-format
msgid "{class_name} must implement get_required_permission()" msgid "{class_name} must implement get_required_permission()"
msgstr "" msgstr ""
#: netbox/utilities/views.py:100 #: netbox/utilities/views.py:104
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only " "{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only "

View File

@ -29,7 +29,7 @@ def linkify_phone(value):
""" """
if value is None: if value is None:
return None return None
return f"tel:{value}" return f"tel:{value.replace(' ', '')}"
def register_table_column(column, name, *tables): def register_table_column(column, name, *tables):

View File

@ -11,7 +11,7 @@
{% elif customfield.type == 'date' and value %} {% elif customfield.type == 'date' and value %}
{{ value|isodate }} {{ value|isodate }}
{% elif customfield.type == 'datetime' and value %} {% elif customfield.type == 'datetime' and value %}
{{ value|isodate }} {{ value|isodatetime }} {{ value|isodatetime }}
{% elif customfield.type == 'url' and value %} {% elif customfield.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a> <a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif customfield.type == 'json' and value %} {% elif customfield.type == 'json' and value %}

View File

@ -1,4 +1,5 @@
from django import template from django import template
from django.utils.safestring import mark_safe
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from utilities.querydict import dict_to_querydict from utilities.querydict import dict_to_querydict
@ -124,5 +125,5 @@ def formaction(context):
if HTMX navigation is enabled (per the user's preferences). if HTMX navigation is enabled (per the user's preferences).
""" """
if context.get('htmx_navigation', False): if context.get('htmx_navigation', False):
return 'hx-push-url="true" hx-post' return mark_safe('hx-push-url="true" hx-post')
return 'formaction' return 'formaction'

View File

@ -281,6 +281,10 @@ def applied_filters(context, model, form, query_params):
if filter_name not in querydict: if filter_name not in querydict:
continue continue
# Skip saved filters, as they're displayed alongside the quick search widget
if filter_name == 'filter_id':
continue
bound_field = form.fields[filter_name].get_bound_field(form, filter_name) bound_field = form.fields[filter_name].get_bound_field(form, filter_name)
querydict.pop(filter_name) querydict.pop(filter_name)
display_value = ', '.join([str(v) for v in get_selected_values(form, filter_name)]) display_value = ', '.join([str(v) for v in get_selected_values(form, filter_name)])

View File

@ -1,3 +1,5 @@
from typing import Iterable
from django.contrib.auth.mixins import AccessMixin from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse from django.urls import reverse
@ -6,10 +8,12 @@ from django.utils.translation import gettext_lazy as _
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
from netbox.registry import registry from netbox.registry import registry
from utilities.relations import get_related_models
from .permissions import resolve_permission from .permissions import resolve_permission
__all__ = ( __all__ = (
'ContentTypePermissionRequiredMixin', 'ContentTypePermissionRequiredMixin',
'GetRelatedModelsMixin',
'GetReturnURLMixin', 'GetReturnURLMixin',
'ObjectPermissionRequiredMixin', 'ObjectPermissionRequiredMixin',
'ViewTab', 'ViewTab',
@ -142,6 +146,46 @@ class GetReturnURLMixin:
return reverse('home') return reverse('home')
class GetRelatedModelsMixin:
"""
Provides logic for collecting all related models for the currently viewed model.
"""
def get_related_models(self, request, instance, omit=[], extra=[]):
"""
Get related models of the view's `queryset` model without those listed in `omit`. Will be sorted alphabetical.
Args:
request: Current request being processed.
instance: The instance related models should be looked up for. A list of instances can be passed to match
related objects in this list (e.g. to find sites of a region including child regions).
omit: Remove relationships to these models from the result. Needs to be passed, if related models don't
provide a `_list` view.
extra: Add extra models to the list of automatically determined related models. Can be used to add indirect
relationships.
"""
model = self.queryset.model
related = filter(
lambda m: m[0] is not model and m[0] not in omit,
get_related_models(model, False)
)
related_models = [
(
model.objects.restrict(request.user, 'view').filter(**(
{f'{field}__in': instance}
if isinstance(instance, Iterable)
else {field: instance}
)),
f'{field}_id'
)
for model, field in related
]
related_models.extend(extra)
return sorted(related_models, key=lambda x: x[0].model._meta.verbose_name.lower())
class ViewTab: class ViewTab:
""" """
ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for

View File

@ -20,7 +20,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.query import count_related from utilities.query import count_related
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.views import ViewTab, register_model_view from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -39,16 +39,12 @@ class ClusterTypeListView(generic.ObjectListView):
@register_model_view(ClusterType) @register_model_view(ClusterType)
class ClusterTypeView(generic.ObjectView): class ClusterTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ClusterType.objects.all() queryset = ClusterType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Cluster.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -99,16 +95,12 @@ class ClusterGroupListView(generic.ObjectListView):
@register_model_view(ClusterGroup) @register_model_view(ClusterGroup)
class ClusterGroupView(generic.ObjectView): class ClusterGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ClusterGroup.objects.all() queryset = ClusterGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Cluster.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -2,7 +2,7 @@ from ipam.tables import RouteTargetTable
from netbox.views import generic from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.query import count_related from utilities.query import count_related
from utilities.views import register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -21,16 +21,12 @@ class TunnelGroupListView(generic.ObjectListView):
@register_model_view(TunnelGroup) @register_model_view(TunnelGroup)
class TunnelGroupView(generic.ObjectView): class TunnelGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = TunnelGroup.objects.all() queryset = TunnelGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Tunnel.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -1,7 +1,7 @@
from dcim.models import Interface from dcim.models import Interface
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.views import register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -24,17 +24,14 @@ class WirelessLANGroupListView(generic.ObjectListView):
@register_model_view(WirelessLANGroup) @register_model_view(WirelessLANGroup)
class WirelessLANGroupView(generic.ObjectView): class WirelessLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = WirelessLANGroup.objects.all() queryset = WirelessLANGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
related_models = (
(WirelessLAN.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, groups),
} }