Merge pull request #8185 from netbox-community/develop

Release v3.1.3
This commit is contained in:
Jeremy Stretch 2021-12-29 12:31:07 -05:00 committed by GitHub
commit 1f575a2a47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 1069 additions and 1390 deletions

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.1.2
placeholder: v3.1.3
validations:
required: true
- type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.1.2
placeholder: v3.1.3
validations:
required: true
- type: dropdown

View File

@ -55,3 +55,7 @@ The link will only appear when viewing a device with a manufacturer name of "Cis
## Link Groups
Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group.
## Table Columns
Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL.

View File

@ -1,5 +1,31 @@
# NetBox v3.1
## v3.1.3 (2021-12-29)
### Enhancements
* [#6782](https://github.com/netbox-community/netbox/issues/6782) - Enable the inclusion of custom links in tables
* [#7600](https://github.com/netbox-community/netbox/issues/7600) - Include count of available IPs on prefix view
* [#8034](https://github.com/netbox-community/netbox/issues/8034) - Enable specifying custom field validators during CSV import
* [#8100](https://github.com/netbox-community/netbox/issues/8100) - Add "other" choice for FHRP group protocol
* [#8175](https://github.com/netbox-community/netbox/issues/8175) - Display parent object when attaching an image
### Bug Fixes
* [#7246](https://github.com/netbox-community/netbox/issues/7246) - Don't attempt to URL-decode NAPALM response payloads
* [#7290](https://github.com/netbox-community/netbox/issues/7290) - Defer loading API-backed form fields
* [#7887](https://github.com/netbox-community/netbox/issues/7887) - Forward `HTTP_X_FORWARDED_FOR` to custom scripts
* [#7962](https://github.com/netbox-community/netbox/issues/7962) - Fix user menu under report/script result view
* [#7972](https://github.com/netbox-community/netbox/issues/7972) - Standardize name of `RemoteUserBackend` logger
* [#8097](https://github.com/netbox-community/netbox/issues/8097) - Fix styling of Markdown tables
* [#8127](https://github.com/netbox-community/netbox/issues/8127) - Fix disassociation of interface under IP address edit view
* [#8131](https://github.com/netbox-community/netbox/issues/8131) - Restore annotation of available IPs under prefix IPs view
* [#8134](https://github.com/netbox-community/netbox/issues/8134) - Fix bulk editing of objects within dynamic tables
* [#8139](https://github.com/netbox-community/netbox/issues/8139) - Fix rendering of table configuration form under VM interfaces view
* [#8140](https://github.com/netbox-community/netbox/issues/8140) - Restore missing fields on wireless LAN & link REST API serializers
---
## v3.1.2 (2021-12-20)
### Enhancements

View File

@ -26,14 +26,12 @@ class ProviderFilterForm(CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -42,8 +40,7 @@ class ProviderFilterForm(CustomFieldModelFilterForm):
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
asn = forms.IntegerField(
required=False,
@ -61,8 +58,7 @@ class ProviderNetworkFilterForm(CustomFieldModelFilterForm):
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider'),
fetch_trigger='open'
label=_('Provider')
)
tag = TagFilterField(model)
@ -84,14 +80,12 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
required=False,
label=_('Type'),
fetch_trigger='open'
label=_('Type')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider'),
fetch_trigger='open'
label=_('Provider')
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
@ -99,8 +93,7 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network'),
fetch_trigger='open'
label=_('Provider network')
)
status = forms.MultipleChoiceField(
choices=CircuitStatusChoices,
@ -110,14 +103,12 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -126,8 +117,7 @@ class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'region_id': '$region_id',
'site_group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
commit_rate = forms.IntegerField(
required=False,

View File

@ -15,14 +15,14 @@ from circuits.models import Circuit
from dcim import filtersets
from dcim.models import *
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
from ipam.models import Prefix, VLAN, ASN
from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.views import ModelViewSet
from netbox.config import get_config
from utilities.api import get_serializer_for_model
from utilities.utils import count_related, decode_dict
from utilities.utils import count_related
from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
@ -501,7 +501,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
continue
try:
response[method] = decode_dict(getattr(d, method)())
response[method] = getattr(d, method)()
except NotImplementedError:
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
except Exception as e:

View File

@ -57,14 +57,12 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -73,8 +71,7 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@ -82,14 +79,12 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id',
},
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
virtual_chassis_id = DynamicModelMultipleChoiceField(
queryset=VirtualChassis.objects.all(),
required=False,
label=_('Virtual Chassis'),
fetch_trigger='open'
label=_('Virtual Chassis')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@ -99,8 +94,7 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm):
'location_id': '$location_id',
'virtual_chassis_id': '$virtual_chassis_id'
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)
@ -109,8 +103,7 @@ class RegionFilterForm(CustomFieldModelFilterForm):
parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Parent region'),
fetch_trigger='open'
label=_('Parent region')
)
tag = TagFilterField(model)
@ -120,8 +113,7 @@ class SiteGroupFilterForm(CustomFieldModelFilterForm):
parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Parent group'),
fetch_trigger='open'
label=_('Parent group')
)
tag = TagFilterField(model)
@ -142,20 +134,17 @@ class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
asn_id = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
required=False,
label=_('ASNs'),
fetch_trigger='open'
label=_('ASNs')
)
tag = TagFilterField(model)
@ -170,14 +159,12 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -186,8 +173,7 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
parent_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@ -196,8 +182,7 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'region_id': '$region_id',
'site_id': '$site_id',
},
label=_('Parent'),
fetch_trigger='open'
label=_('Parent')
)
tag = TagFilterField(model)
@ -219,8 +204,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -228,8 +212,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@ -238,8 +221,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
status = forms.MultipleChoiceField(
choices=RackStatusChoices,
@ -260,8 +242,7 @@ class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=RackRole.objects.all(),
required=False,
null_option='None',
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
serial = forms.CharField(
required=False
@ -280,8 +261,7 @@ class RackElevationFilterForm(RackFilterForm):
query_params={
'site_id': '$site_id',
'location_id': '$location_id',
},
fetch_trigger='open'
}
)
@ -296,8 +276,7 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -305,15 +284,13 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.prefetch_related('site'),
required=False,
label=_('Location'),
null_option='None',
fetch_trigger='open'
null_option='None'
)
user_id = DynamicModelMultipleChoiceField(
queryset=User.objects.all(),
@ -321,8 +298,7 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
),
fetch_trigger='open'
)
)
tag = TagFilterField(model)
@ -342,8 +318,7 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
subdevice_role = forms.MultipleChoiceField(
choices=add_blank_choice(SubdeviceRoleChoices),
@ -410,8 +385,7 @@ class PlatformFilterForm(CustomFieldModelFilterForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
tag = TagFilterField(model)
@ -432,14 +406,12 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -448,8 +420,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@ -458,8 +429,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
query_params={
'site_id': '$site_id'
},
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
@ -469,20 +439,17 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
'site_id': '$site_id',
'location_id': '$location_id',
},
label=_('Rack'),
fetch_trigger='open'
label=_('Rack')
)
role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
device_type_id = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
@ -490,15 +457,13 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi
query_params={
'manufacturer_id': '$manufacturer_id'
},
label=_('Model'),
fetch_trigger='open'
label=_('Model')
)
platform_id = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
required=False,
null_option='None',
label=_('Platform'),
fetch_trigger='open'
label=_('Platform')
)
status = forms.MultipleChoiceField(
choices=DeviceStatusChoices,
@ -589,14 +554,12 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -605,8 +568,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
tag = TagFilterField(model)
@ -622,8 +584,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -631,8 +592,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
@ -641,8 +601,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
null_option='None',
query_params={
'site_id': '$site_id'
},
fetch_trigger='open'
}
)
type = forms.MultipleChoiceField(
choices=add_blank_choice(CableTypeChoices),
@ -665,8 +624,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
'tenant_id': '$tenant_id',
'rack_id': '$rack_id',
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)
tag = TagFilterField(model)
@ -680,14 +638,12 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -696,8 +652,7 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm):
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
@ -706,8 +661,7 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
tag = TagFilterField(model)
@ -723,14 +677,12 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -738,8 +690,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
power_panel_id = DynamicModelMultipleChoiceField(
queryset=PowerPanel.objects.all(),
@ -748,8 +699,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Power panel'),
fetch_trigger='open'
label=_('Power panel')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
@ -758,8 +708,7 @@ class PowerFeedFilterForm(CustomFieldModelFilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Rack'),
fetch_trigger='open'
label=_('Rack')
)
status = forms.MultipleChoiceField(
choices=PowerFeedStatusChoices,
@ -990,8 +939,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
serial = forms.CharField(
required=False
@ -1016,8 +964,7 @@ class ConsoleConnectionFilterForm(FilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -1025,8 +972,7 @@ class ConsoleConnectionFilterForm(FilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@ -1034,8 +980,7 @@ class ConsoleConnectionFilterForm(FilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)
@ -1043,8 +988,7 @@ class PowerConnectionFilterForm(FilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -1052,8 +996,7 @@ class PowerConnectionFilterForm(FilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@ -1061,8 +1004,7 @@ class PowerConnectionFilterForm(FilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)
@ -1070,8 +1012,7 @@ class InterfaceConnectionFilterForm(FilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -1079,8 +1020,7 @@ class InterfaceConnectionFilterForm(FilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@ -1088,6 +1028,5 @@ class InterfaceConnectionFilterForm(FilterForm):
query_params={
'site_id': '$site_id'
},
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)

View File

@ -301,16 +301,14 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm):
required=False,
initial_params={
'sites': '$site'
},
fetch_trigger='open'
}
)
site_group = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
initial_params={
'sites': '$site'
},
fetch_trigger='open'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
@ -318,24 +316,21 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm):
query_params={
'region_id': '$region',
'group_id': '$site_group',
},
fetch_trigger='open'
}
)
location = DynamicModelChoiceField(
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
},
fetch_trigger='open'
}
)
rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
query_params={
'site_id': '$site',
'location_id': '$location',
},
fetch_trigger='open'
}
)
units = NumericArrayField(
base_field=forms.IntegerField(),
@ -349,8 +344,7 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm):
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False,
fetch_trigger='open'
required=False
)
class Meta:

View File

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

View File

@ -111,8 +111,7 @@ class ComponentTemplateTable(BaseTable):
class ConsolePortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=ConsolePortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_consoleports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@ -124,8 +123,7 @@ class ConsolePortTemplateTable(ComponentTemplateTable):
class ConsoleServerPortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=ConsoleServerPortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_consoleserverports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@ -137,8 +135,7 @@ class ConsoleServerPortTemplateTable(ComponentTemplateTable):
class PowerPortTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=PowerPortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_powerports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@ -150,8 +147,7 @@ class PowerPortTemplateTable(ComponentTemplateTable):
class PowerOutletTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=PowerOutletTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_poweroutlets'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@ -166,8 +162,7 @@ class InterfaceTemplateTable(ComponentTemplateTable):
)
actions = ButtonsColumn(
model=InterfaceTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_interfaces'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@ -183,8 +178,7 @@ class FrontPortTemplateTable(ComponentTemplateTable):
color = ColorColumn()
actions = ButtonsColumn(
model=FrontPortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_frontports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@ -197,8 +191,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
color = ColorColumn()
actions = ButtonsColumn(
model=RearPortTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_rearports'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):
@ -210,8 +203,7 @@ class RearPortTemplateTable(ComponentTemplateTable):
class DeviceBayTemplateTable(ComponentTemplateTable):
actions = ButtonsColumn(
model=DeviceBayTemplate,
buttons=('edit', 'delete'),
return_url_extra='%23tab_devicebays'
buttons=('edit', 'delete')
)
class Meta(ComponentTemplateTable.Meta):

View File

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

View File

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

View File

@ -3,9 +3,10 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe
from extras.choices import CustomFieldTypeChoices
from extras.models import *
from extras.utils import FeatureQuery
from utilities.forms import CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField
__all__ = (
'CustomFieldCSVForm',
@ -22,6 +23,10 @@ class CustomFieldCSVForm(CSVModelForm):
limit_choices_to=FeatureQuery('custom_fields'),
help_text="One or more assigned object types"
)
type = CSVChoiceField(
choices=CustomFieldTypeChoices,
help_text='Field data type (e.g. text, integer, etc.)'
)
choices = SimpleArrayField(
base_field=forms.CharField(),
required=False,
@ -32,7 +37,7 @@ class CustomFieldCSVForm(CSVModelForm):
model = CustomField
fields = (
'name', 'label', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic', 'default',
'choices', 'weight',
'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
)

View File

@ -164,69 +164,58 @@ class ConfigContextFilterForm(FilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Regions'),
fetch_trigger='open'
label=_('Regions')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site groups'),
fetch_trigger='open'
label=_('Site groups')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Sites'),
fetch_trigger='open'
label=_('Sites')
)
device_type_id = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
required=False,
label=_('Device types'),
fetch_trigger='open'
label=_('Device types')
)
role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Roles'),
fetch_trigger='open'
label=_('Roles')
)
platform_id = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
required=False,
label=_('Platforms'),
fetch_trigger='open'
label=_('Platforms')
)
cluster_group_id = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
label=_('Cluster groups'),
fetch_trigger='open'
label=_('Cluster groups')
)
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
label=_('Clusters'),
fetch_trigger='open'
label=_('Clusters')
)
tenant_group_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label=_('Tenant groups'),
fetch_trigger='open'
label=_('Tenant groups')
)
tenant_id = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
required=False,
label=_('Tenant'),
fetch_trigger='open'
label=_('Tenant')
)
tag = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
required=False,
label=_('Tags'),
fetch_trigger='open'
label=_('Tags')
)
@ -263,8 +252,7 @@ class JournalEntryFilterForm(FilterForm):
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
),
fetch_trigger='open'
)
)
assigned_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(),
@ -272,8 +260,7 @@ class JournalEntryFilterForm(FilterForm):
label=_('Object Type'),
widget=APISelectMultiple(
api_url='/api/extras/content-types/',
),
fetch_trigger='open'
)
)
kind = forms.ChoiceField(
choices=add_blank_choice(JournalEntryKindChoices),
@ -310,8 +297,7 @@ class ObjectChangeFilterForm(FilterForm):
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
),
fetch_trigger='open'
)
)
changed_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(),
@ -319,6 +305,5 @@ class ObjectChangeFilterForm(FilterForm):
label=_('Object Type'),
widget=APISelectMultiple(
api_url='/api/extras/content-types/',
),
fetch_trigger='open'
)
)

View File

@ -229,6 +229,24 @@ class CustomLink(ChangeLoggedModel):
def get_absolute_url(self):
return reverse('extras:customlink', args=[self.pk])
def render(self, context):
"""
Render the CustomLink given the provided context, and return the text, link, and link_target.
:param context: The context passed to Jinja2
"""
text = render_jinja2(self.link_text, context)
if not text:
return {}
link = render_jinja2(self.link_url, context)
link_target = ' target="_blank"' if self.new_window else ''
return {
'text': text,
'link': link,
'link_target': link_target,
}
@extras_features('webhooks', 'export_templates')
class ExportTemplate(ChangeLoggedModel):

View File

@ -62,16 +62,14 @@ def custom_links(context, obj):
# Add non-grouped links
else:
try:
text_rendered = render_jinja2(cl.link_text, link_context)
if text_rendered:
link_rendered = render_jinja2(cl.link_url, link_context)
link_target = ' target="_blank"' if cl.new_window else ''
rendered = cl.render(link_context)
if rendered:
template_code += LINK_BUTTON.format(
link_rendered, link_target, cl.button_class, text_rendered
rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
)
except Exception as e:
template_code += '<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{}">' \
'<i class="mdi mdi-alert"></i> {}</a>\n'.format(e, cl.name)
template_code += f'<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{e}">' \
f'<i class="mdi mdi-alert"></i> {cl.name}</a>\n'
# Add grouped links to template
for group, links in group_names.items():
@ -80,17 +78,15 @@ def custom_links(context, obj):
for cl in links:
try:
text_rendered = render_jinja2(cl.link_text, link_context)
if text_rendered:
link_target = ' target="_blank"' if cl.new_window else ''
link_rendered = render_jinja2(cl.link_url, link_context)
rendered = cl.render(link_context)
if rendered:
links_rendered.append(
GROUP_LINK.format(link_rendered, link_target, text_rendered)
GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text'])
)
except Exception as e:
links_rendered.append(
'<li><a class="dropdown-item" disabled="disabled" title="{}"><span class="text-muted">'
'<i class="mdi mdi-alert"></i> {}</span></a></li>'.format(e, cl.name)
f'<li><a class="dropdown-item" disabled="disabled" title="{e}"><span class="text-muted">'
f'<i class="mdi mdi-alert"></i> {cl.name}</span></a></li>'
)
if links_rendered:

View File

@ -39,10 +39,10 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
'name,label,type,content_types,weight,filter_logic,choices',
'field4,Field 4,text,dcim.site,100,exact,',
'field5,Field 5,integer,dcim.site,100,exact,',
'field6,Field 6,select,dcim.site,100,exact,"A,B,C"',
'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex',
'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3}',
'field5,Field 5,integer,dcim.site,100,exact,,1,100,',
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,',
)
cls.bulk_edit_data = {

View File

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

View File

@ -135,6 +135,7 @@ class FHRPGroupProtocolChoices(ChoiceSet):
PROTOCOL_HSRP = 'hsrp'
PROTOCOL_GLBP = 'glbp'
PROTOCOL_CARP = 'carp'
PROTOCOL_OTHER = 'other'
CHOICES = (
(PROTOCOL_VRRP2, 'VRRPv2'),
@ -142,6 +143,7 @@ class FHRPGroupProtocolChoices(ChoiceSet):
(PROTOCOL_HSRP, 'HSRP'),
(PROTOCOL_GLBP, 'GLBP'),
(PROTOCOL_CARP, 'CARP'),
(PROTOCOL_OTHER, 'Other'),
)

View File

@ -48,14 +48,12 @@ class VRFFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
import_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Import targets'),
fetch_trigger='open'
label=_('Import targets')
)
export_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(),
required=False,
label=_('Export targets'),
fetch_trigger='open'
label=_('Export targets')
)
tag = TagFilterField(model)
@ -70,14 +68,12 @@ class RouteTargetFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
importing_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Imported by VRF'),
fetch_trigger='open'
label=_('Imported by VRF')
)
exporting_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Exported by VRF'),
fetch_trigger='open'
label=_('Exported by VRF')
)
tag = TagFilterField(model)
@ -110,8 +106,7 @@ class AggregateFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(),
required=False,
label=_('RIR'),
fetch_trigger='open'
label=_('RIR')
)
tag = TagFilterField(model)
@ -127,14 +122,12 @@ class ASNFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(),
required=False,
label=_('RIR'),
fetch_trigger='open'
label=_('RIR')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
@ -180,14 +173,12 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global',
fetch_trigger='open'
null_option='Global'
)
present_in_vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Present in VRF'),
fetch_trigger='open'
label=_('Present in VRF')
)
status = forms.MultipleChoiceField(
choices=PrefixStatusChoices,
@ -197,14 +188,12 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -213,15 +202,13 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region_id': '$region_id'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
role_id = DynamicModelMultipleChoiceField(
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
is_pool = forms.NullBooleanField(
required=False,
@ -257,8 +244,7 @@ class IPRangeFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global',
fetch_trigger='open'
null_option='Global'
)
status = forms.MultipleChoiceField(
choices=PrefixStatusChoices,
@ -269,8 +255,7 @@ class IPRangeFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
tag = TagFilterField(model)
@ -308,14 +293,12 @@ class IPAddressFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=VRF.objects.all(),
required=False,
label=_('Assigned VRF'),
null_option='Global',
fetch_trigger='open'
null_option='Global'
)
present_in_vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('Present in VRF'),
fetch_trigger='open'
label=_('Present in VRF')
)
status = forms.MultipleChoiceField(
choices=IPAddressStatusChoices,
@ -376,32 +359,27 @@ class VLANGroupFilterForm(CustomFieldModelFilterForm):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
sitegroup = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
location = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
label=_('Location'),
fetch_trigger='open'
label=_('Location')
)
rack = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
label=_('Rack'),
fetch_trigger='open'
label=_('Rack')
)
tag = TagFilterField(model)
@ -417,14 +395,12 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region'),
fetch_trigger='open'
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group'),
fetch_trigger='open'
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
@ -433,8 +409,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region': '$region'
},
label=_('Site'),
fetch_trigger='open'
label=_('Site')
)
group_id = DynamicModelMultipleChoiceField(
queryset=VLANGroup.objects.all(),
@ -443,8 +418,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
query_params={
'region': '$region'
},
label=_('VLAN group'),
fetch_trigger='open'
label=_('VLAN group')
)
status = forms.MultipleChoiceField(
choices=VLANStatusChoices,
@ -455,8 +429,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm):
queryset=Role.objects.all(),
required=False,
null_option='None',
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
vid = forms.IntegerField(
required=False,

View File

@ -471,6 +471,8 @@ class IPAddressForm(TenancyForm, CustomFieldModelForm):
})
elif selected_objects:
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
else:
self.instance.assigned_object = None
# Primary IP assignment is only available if an interface has been assigned.
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')

View File

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

View File

@ -105,7 +105,7 @@ class RemoteUserBackend(_RemoteUserBackend):
return settings.REMOTE_AUTH_AUTO_CREATE_USER
def configure_groups(self, user, remote_groups):
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
# Assign default groups to the user
group_list = []
@ -141,7 +141,7 @@ class RemoteUserBackend(_RemoteUserBackend):
Return None if ``create_unknown_user`` is ``False`` and a ``User``
object with the given username is not found in the database.
"""
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
logger.debug(
f"trying to authenticate {remote_user} with groups {remote_groups}")
if not remote_user:
@ -173,7 +173,7 @@ class RemoteUserBackend(_RemoteUserBackend):
return None
def _is_superuser(self, user):
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
superuser_groups = settings.REMOTE_AUTH_SUPERUSER_GROUPS
logger.debug(f"Superuser Groups: {superuser_groups}")
superusers = settings.REMOTE_AUTH_SUPERUSERS
@ -189,7 +189,7 @@ class RemoteUserBackend(_RemoteUserBackend):
return bool(result)
def _is_staff(self, user):
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
staff_groups = settings.REMOTE_AUTH_STAFF_GROUPS
logger.debug(f"Superuser Groups: {staff_groups}")
staff_users = settings.REMOTE_AUTH_STAFF_USERS
@ -204,7 +204,7 @@ class RemoteUserBackend(_RemoteUserBackend):
return bool(result)
def configure_user(self, request, user):
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
if not settings.REMOTE_AUTH_GROUP_SYNC_ENABLED:
# Assign default groups to the user
group_list = []

View File

@ -19,7 +19,7 @@ from netbox.config import PARAMS
# Environment setup
#
VERSION = '3.1.2'
VERSION = '3.1.3'
# Hostname
HOSTNAME = platform.node()

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -35,11 +35,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
for (const element of form.querySelectorAll<FormControls>('*[name]')) {
if (!element.validity.valid) {
invalids.add(element.name);
// If the field is invalid, but contains the .is-valid class, remove it.
if (element.classList.contains('is-valid')) {
element.classList.remove('is-valid');
}
// If the field is invalid, but doesn't contain the .is-invalid class, add it.
if (!element.classList.contains('is-invalid')) {
element.classList.add('is-invalid');
@ -49,10 +44,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
if (element.classList.contains('is-invalid')) {
element.classList.remove('is-invalid');
}
// If the field is valid, but doesn't contain the .is-valid class, add it.
if (!element.classList.contains('is-valid')) {
element.classList.add('is-valid');
}
}
}

View File

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

View File

@ -0,0 +1,23 @@
import { getElements, isTruthy } from './util';
import { initButtons } from './buttons';
function initDepedencies(): void {
for (const init of [initButtons]) {
init();
}
}
/**
* Hook into HTMX's event system to reinitialize specific native event listeners when HTMX swaps
* elements.
*/
export function initHtmx(): void {
for (const element of getElements('[hx-target]')) {
const targetSelector = element.getAttribute('hx-target');
if (isTruthy(targetSelector)) {
for (const target of getElements(targetSelector)) {
target.addEventListener('htmx:afterSettle', initDepedencies);
}
}
}
}

View File

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

View File

@ -12,6 +12,7 @@ import { initInterfaceTable } from './tables';
import { initSideNav } from './sidenav';
import { initRackElevation } from './racks';
import { initLinks } from './links';
import { initHtmx } from './htmx';
function initDocument(): void {
for (const init of [
@ -29,6 +30,7 @@ function initDocument(): void {
initSideNav,
initRackElevation,
initLinks,
initHtmx,
]) {
init();
}

View File

@ -251,7 +251,7 @@ export class APISelect {
} else if (collapse !== null) {
this.trigger = 'collapse';
} else {
this.trigger = 'load';
this.trigger = 'open';
}
switch (this.trigger) {

View File

@ -965,6 +965,19 @@ div.card-overlay {
max-width: unset;
}
/* Rendered Markdown */
.rendered-markdown table {
width: 100%;
}
.rendered-markdown th {
border-bottom: 2px solid #dddddd;
padding: 8px;
}
.rendered-markdown td {
border-top: 1px solid #dddddd;
padding: 8px;
}
// Preformatted text blocks
td pre {
margin-bottom: 0

View File

@ -8,6 +8,7 @@ $theme-colors: map-merge(
$theme-colors,
(
'primary': #337ab7,
'info': #54d6f0,
'red': $red-500,
'yellow': $yellow-500,
'green': $green-500,

View File

@ -104,23 +104,23 @@
{# Static resources #}
<link
rel="stylesheet"
href="{% static 'netbox-external.css'%}"
href="{% static 'netbox-external.css'%}?v={{ settings.VERSION }}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'"
/>
<link
rel="stylesheet"
href="{% static 'netbox-light.css'%}"
href="{% static 'netbox-light.css'%}?v={{ settings.VERSION }}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox-light.css'"
/>
<link
rel="stylesheet"
href="{% static 'netbox-dark.css'%}"
href="{% static 'netbox-dark.css'%}?v={{ settings.VERSION }}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox-dark.css'"
/>
<link
rel="stylesheet"
media="print"
href="{% static 'netbox-print.css'%}"
href="{% static 'netbox-print.css'%}?v={{ settings.VERSION }}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox-print.css'"
/>
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
@ -129,7 +129,7 @@
{# Javascript #}
<script
type="text/javascript"
src="{% static 'netbox.js' %}"
src="{% static 'netbox.js' %}?v={{ settings.VERSION }}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
</script>

View File

@ -1,8 +1,7 @@
{# Base layout for the core NetBox UI w/navbar and page content #}
{% extends 'base/base.html' %}
{% load helpers %}
{% load nav %}
{% load search_options %}
{% load search %}
{% load static %}
{% block layout %}
@ -21,7 +20,7 @@
</div>
{# Top bar #}
<nav class="navbar navbar-light sticky-top flex-md-nowrap ps-6 p-3 search container-fluid noprint">
<nav class="navbar navbar-light sticky-top flex-md-nowrap p-1 mb-3 search container-fluid border-bottom noprint">
{# Mobile Navigation #}
<div class="nav-mobile">

View File

@ -1,4 +1,4 @@
{% load nav %}
{% load navigation %}
{% load static %}
<nav class="sidenav noprint" id="sidenav" data-simplebar>

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% block title %}Swap Circuit Terminations{% endblock %}

View File

@ -10,7 +10,7 @@
</a>
{% endif %}
{% if termination and perms.circuits.change_circuittermination %}
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}" class="btn btn-sm btn-yellow lh-1">
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}" class="btn btn-sm btn-warning lh-1">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
</a>
<a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}" class="btn btn-sm btn-primary lh-1">

View File

@ -41,11 +41,11 @@
</tr>
<tr>
<th scope="row">NOC Contact</th>
<td class="rendered-markdown">{{ object.noc_contact|render_markdown|placeholder }}</td>
<td>{{ object.noc_contact|render_markdown|placeholder }}</td>
</tr>
<tr>
<th scope="row">Admin Contact</th>
<td class="rendered-markdown">{{ object.admin_contact|render_markdown|placeholder }}</td>
<td>{{ object.admin_contact|render_markdown|placeholder }}</td>
</tr>
<tr>
<th scope="row">Circuits</th>

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load helpers %}
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete console port {{ consoleport }}?{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete console server port {{ consoleserverport }}?{% endblock %}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete device bay {{ devicebay }}?{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Remove {{ device_bay.installed_device }} from {{ device_bay }}?{% endblock %}

View File

@ -40,13 +40,7 @@
</tr>
<tr>
<th scope="row">VM Role</th>
<td>
{% if object.vm_role %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.vm_role %}</td>
</tr>
<tr>
<th scope="row">Devices</th>

View File

@ -33,13 +33,7 @@
</tr>
<tr>
<td>Full Depth</td>
<td>
{% if object.is_full_depth %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.is_full_depth %}</td>
</tr>
<tr>
<td>Parent/Child</td>

View File

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

View File

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

View File

@ -48,23 +48,11 @@
</tr>
<tr>
<th scope="row">Enabled</th>
<td>
{% if object.enabled %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.enabled %}</td>
</tr>
<tr>
<th scope="row">Management Only</th>
<td>
{% if object.mgmt_only %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.mgmt_only %}</td>
</tr>
<tr>
<th scope="row">Parent</th>

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete interface {{ interface }}?{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete inventory item {{ inventoryitem }}?{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete power outlet {{ poweroutlet }}?{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete power port {{ powerport }}?{% endblock %}

View File

@ -65,7 +65,7 @@
</td>
<td>
{% if object.master == vc_member %}
<i class="mdi mdi-check-bold text-success"></i>
{% checkmark True %}
{% endif %}
</td>
<td>

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Remove Virtual Chassis Member?{% endblock %}

View File

@ -29,13 +29,7 @@
</tr>
<tr>
<th scope="row">Required</th>
<td>
{% if object.required %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.required %}</td>
</tr>
<tr>
<th scope="row">Weight</th>

View File

@ -33,13 +33,7 @@
</tr>
<tr>
<th scope="row">New Window</th>
<td>
{% if object.new_window %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.new_window %}</td>
</tr>
</table>
</div>

View File

@ -40,13 +40,7 @@
</tr>
<tr>
<th scope="row">Attachment</th>
<td>
{% if object.as_attachment %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.as_attachment %}</td>
</tr>
</table>
</div>

View File

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

View File

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

View File

@ -0,0 +1,16 @@
{% extends 'generic/object_edit.html' %}
{% load helpers %}
{% block form_fields %}
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end required">
{{ obj.parent|meta:"verbose_name"|bettertitle }}
</label>
<div class="col-sm-9">
<div class="form-control-plaintext">
<a href="{{ obj.parent.get_absolute_url }}" class="">{{ obj.parent }}</a>
</div>
</div>
</div>
{{ block.super }}
{% endblock form_fields %}

View File

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

View File

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

View File

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

View File

@ -17,13 +17,7 @@
</tr>
<tr>
<th scope="row">Enabled</th>
<td>
{% if object.enabled %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.enabled %}</td>
</tr>
</table>
</div>
@ -36,33 +30,15 @@
<table class="table table-hover attr-table">
<tr>
<th scope="row">Create</th>
<td>
{% if object.type_create %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.type_create %}</td>
</tr>
<tr>
<th scope="row">Update</th>
<td>
{% if object.type_update %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.type_update %}</td>
</tr>
<tr>
<th scope="row">Delete</th>
<td>
{% if object.type_delete %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.type_delete %}</td>
</tr>
</table>
</div>
@ -100,13 +76,7 @@
<table class="table table-hover attr-table">
<tr>
<th scope="row">SSL Verification</th>
<td>
{% if object.ssl_verification %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.ssl_verification %}</td>
</tr>
<tr>
<th scope="row">CA File Path</th>

View File

@ -66,7 +66,7 @@
</td>
<td>
{% if field.required %}
<i class="mdi mdi-check-bold text-success" title="Required"></i>
{% checkmark True true="Required" %}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}

View File

@ -1,4 +1,4 @@
{% extends 'utilities/confirmation_form.html' %}
{% extends 'generic/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete {{ obj_type }}?{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends 'base/layout.html' %}
{% load get_status %}
{% load helpers %}
{% load render_table from django_tables2 %}
@ -24,7 +23,7 @@
{% block title %}Home{% endblock %}
{% block content-wrapper %}
<div class="p-3">
<div class="px-3">
{# General stats #}
<div class="row masonry">
{% for section, items, icon in stats %}

View File

@ -4,7 +4,7 @@
<h5 class="card-header">
Comments
</h5>
<div class="card-body rendered-markdown">
<div class="card-body">
{% if object.comments %}
{{ object.comments|render_markdown }}
{% else %}

View File

@ -15,9 +15,9 @@
{% if field.type == 'longtext' and value %}
{{ value|render_markdown }}
{% elif field.type == 'boolean' and value == True %}
<i class="mdi mdi-check-bold text-success" title="True"></i>
{% checkmark value true="True" %}
{% elif field.type == 'boolean' and value == False %}
<i class="mdi mdi-close-thick text-danger" title="False"></i>
{% checkmark value false="False" %}
{% elif field.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif field.type == 'json' and value %}

View File

@ -1,30 +0,0 @@
{% load helpers %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for section_name, menu_items in registry.plugin_menu_items.items %}
<li class="dropdown-header">{{ section_name }}</li>
{% for menu_item in menu_items %}
{% if not menu_item.permissions or request.user|has_perms:menu_item.permissions %}
<li>
{% if menu_item.buttons %}
<div class="buttons float-end">
{% for button in menu_item.buttons %}
{% if not button.permissions or request.user|has_perms:button.permissions %}
<a href="{% url button.link %}" class="btn btn-sm btn-{{ button.color }}" title="{{ button.title }}"><i class="{{ button.icon_class }}"></i></a>
{% endif %}
{% endfor %}
</div>
{% endif %}
<a href="{% url menu_item.link %}">{{ menu_item.link_text }}</a>
</li>
{% else %}
<li class="disabled"><a href="#">{{ menu_item.link_text }}</a></li>
{% endif %}
{% endfor %}
{% if not forloop.last %}
<li class="divider"></li>
{% endif %}
{% endfor %}
</ul>
</li>

View File

@ -1,60 +1,56 @@
{% if request.user.is_authenticated %}
<span class="dropdown profile-button">
<button
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
class="btn btn-outline-secondary dropdown-toggle w-100"
>
<i class="mdi mdi-account"></i>
<span id="navbar_user">{{ request.user|truncatechars:"30" }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button type="button" class="dropdown-item color-mode-toggle">
<i class="color-mode-icon mdi mdi-lightbulb"></i>&nbsp;
<span class="color-mode-text">Dark Mode</span>
</button>
</li>
<li>
{% if request.user.is_staff %}
<a class="dropdown-item" href="{% url 'admin:index' %}">
<i class="mdi mdi-cog"></i> Admin
</a>
{% endif %}
</li>
<li>
<a class="dropdown-item" href="{% url 'user:profile' %}">
<i class="mdi mdi-account"></i> Profile & Settings
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<a class="dropdown-item text-danger" href="{% url 'logout' %}">
<i class="mdi mdi-logout-variant"></i> Log Out
</a>
</li>
</ul>
</span>
<div class="dropdown profile-button">
<button type="button" aria-expanded="false" data-bs-toggle="dropdown" class="btn btn-outline-secondary dropdown-toggle w-100">
<i class="mdi mdi-account"></i>
<span id="navbar_user">{{ request.user|truncatechars:"30" }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button type="button" class="dropdown-item color-mode-toggle">
<i class="color-mode-icon mdi mdi-lightbulb"></i>
<span class="color-mode-text">Dark Mode</span>
</button>
</li>
<li>
{% if request.user.is_staff %}
<a class="dropdown-item" href="{% url 'admin:index' %}">
<i class="mdi mdi-cog"></i> Admin
</a>
{% endif %}
</li>
<li>
<a class="dropdown-item" href="{% url 'user:profile' %}">
<i class="mdi mdi-account"></i> Profile
</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'user:preferences' %}">
<i class="mdi mdi-wrench"></i> Preferences
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<a class="dropdown-item" href="{% url 'logout' %}">
<i class="mdi mdi-logout-variant"></i> Log Out
</a>
</li>
</ul>
</div>
{% else %}
<div class="btn-group">
<a
class="btn btn-primary ws-nowrap"
type="button"
href="{% url 'login' %}"
>
<i class="mdi mdi-login-variant"></i> Log In
</a>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item color-mode-toggle">
<i class="color-mode-icon mdi mdi-lightbulb"></i>&nbsp;
<span class="color-mode-text">Dark Mode</span>
</button>
</li>
</ul>
</div>
<div class="btn-group">
<a class="btn btn-primary ws-nowrap" type="button" href="{% url 'login' %}">
<i class="mdi mdi-login-variant"></i> Log In
</a>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item color-mode-toggle">
<i class="color-mode-icon mdi mdi-lightbulb"></i>
<span class="color-mode-text">Dark Mode</span>
</button>
</li>
</ul>
</div>
{% endif %}

View File

@ -13,143 +13,135 @@
{% block content %}
<div class="row">
<div class="col col-md-4">
<div class="card">
<h5 class="card-header">
IP Address
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Family</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">VRF</th>
<td>
{% if object.vrf %}
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
{% else %}
<span>Global</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<td>
{% if object.tenant %}
{% if object.tenant.group %}
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
{% endif %}
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Status</th>
<td>
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
</td>
</tr>
<tr>
<th scope="row">Role</th>
<td>
{% if object.role %}
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">DNS Name</th>
<td>{{ object.dns_name|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Assignment</th>
<td>
{% if object.assigned_object %}
{% if object.assigned_object.parent_object %}
<a href="{{ object.assigned_object.parent_object.get_absolute_url }}">{{ object.assigned_object.parent_object }}</a> /
{% endif %}
<a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}
<div class="card">
<h5 class="card-header">
IP Address
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Family</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">VRF</th>
<td>
{% if object.vrf %}
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
{% else %}
<span class="text-muted">&mdash;</span>
<span>Global</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">NAT (inside)</th>
<td>
{% if object.nat_inside %}
<a href="{{ object.nat_inside.get_absolute_url }}">{{ object.nat_inside }}</a>
{% if object.nat_inside.assigned_object %}
(<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
{% endif %}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">NAT (outside)</th>
<td>
{% if object.nat_outside %}
<a href="{{ object.nat_outside.get_absolute_url }}">{{ object.nat_outside }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-8">
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
{% if duplicate_ips_table.rows %}
{# Custom version of panel_table.html #}
<div class="card border-danger">
<h5 class="card-header">
<span class="text-danger">Duplicate IP Addresses</span>
{% if more_duplicate_ips %}
<div class="float-end">
<a type="button" class="btn btn-primary btn-sm"
{% if object.vrf %}
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id={{ object.vrf.pk }}"
</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<td>
{% if object.tenant %}
{% if object.tenant.group %}
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
{% endif %}
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Status</th>
<td>
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
</td>
</tr>
<tr>
<th scope="row">Role</th>
<td>
{% if object.role %}
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">DNS Name</th>
<td>{{ object.dns_name|placeholder }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Assignment</th>
<td>
{% if object.assigned_object %}
{% if object.assigned_object.parent_object %}
<a href="{{ object.assigned_object.parent_object.get_absolute_url }}">{{ object.assigned_object.parent_object }}</a> /
{% endif %}
<a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}
{% else %}
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id=null"
<span class="text-muted">&mdash;</span>
{% endif %}
>Show all</a>
</div>
{% endif %}
</h5>
<div class="card-body table-responsive">
{% render_table duplicate_ips_table 'inc/table.html' %}
</div>
</div>
{% endif %}
<div class="my-3">
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
</div>
{% plugin_right_page object %}
</td>
</tr>
<tr>
<th scope="row">NAT (inside)</th>
<td>
{% if object.nat_inside %}
<a href="{{ object.nat_inside.get_absolute_url }}">{{ object.nat_inside }}</a>
{% if object.nat_inside.assigned_object %}
(<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
{% endif %}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">NAT (outside)</th>
<td>
{% if object.nat_outside %}
<a href="{{ object.nat_outside.get_absolute_url }}">{{ object.nat_outside }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_left_page object %}
</div>
</div>
<div class="row my-3">
<div class="col col-md-4">
{% include 'inc/panels/tags.html' %}
<div class="col col-md-8">
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
{% if duplicate_ips_table.rows %}
{# Custom version of panel_table.html #}
<div class="card border-danger">
<h5 class="card-header">
<span class="text-danger">Duplicate IP Addresses</span>
{% if more_duplicate_ips %}
<div class="float-end">
<a type="button" class="btn btn-primary btn-sm"
{% if object.vrf %}
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id={{ object.vrf.pk }}"
{% else %}
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id=null"
{% endif %}
>Show all</a>
</div>
{% endif %}
</h5>
<div class="card-body table-responsive">
{% render_table duplicate_ips_table 'inc/table.html' %}
</div>
</div>
{% endif %}
<div class="my-3">
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">

View File

@ -4,127 +4,154 @@
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Prefix
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Family</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">VRF</th>
<td>
{% if object.vrf %}
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a> ({{ object.vrf.rd }})
{% else %}
<span>Global</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<td>
{% if object.tenant %}
{% if object.tenant.group %}
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
{% endif %}
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Aggregate</th>
<td>
{% if aggregate %}
<a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate.prefix }}</a> ({{ aggregate.rir }})
{% else %}
<span class="text-warning">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Site</th>
<td>
{% if object.site %}
{% if object.site.region %}
<a href="{{ object.site.region.get_absolute_url }}">{{ object.site.region }}</a> /
{% endif %}
<a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">VLAN</th>
<td>
{% if object.vlan %}
{% if object.vlan.group %}
<a href="{{ object.vlan.group.get_absolute_url }}">{{ object.vlan.group }}</a> /
{% endif %}
<a href="{% url 'ipam:vlan' pk=object.vlan.pk %}">{{ object.vlan }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Status</th>
<td>
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
</td>
</tr>
<tr>
<th scope="row">Role</th>
<td>
{% if object.role %}
<a href="{{ object.role.get_absolute_url }}">{{ object.role }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Is a pool</th>
<td>
{% if object.is_pool %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Utilization</th>
<td>
{% if object.mark_utilized %}
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
<small>(Marked fully utilized)</small>
{% else %}
{% utilization_graph object.get_utilization %}
{% endif %}
</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Prefix</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Family</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">VRF</th>
<td>
{% if object.vrf %}
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a> ({{ object.vrf.rd }})
{% else %}
<span>Global</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Tenant</th>
<td>
{% if object.tenant %}
{% if object.tenant.group %}
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
{% endif %}
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Aggregate</th>
<td>
{% if aggregate %}
<a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate.prefix }}</a> ({{ aggregate.rir }})
{% else %}
<span class="text-warning">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Site</th>
<td>
{% if object.site %}
{% if object.site.region %}
<a href="{{ object.site.region.get_absolute_url }}">{{ object.site.region }}</a> /
{% endif %}
<a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">VLAN</th>
<td>
{% if object.vlan %}
{% if object.vlan.group %}
<a href="{{ object.vlan.group.get_absolute_url }}">{{ object.vlan.group }}</a> /
{% endif %}
<a href="{% url 'ipam:vlan' pk=object.vlan.pk %}">{{ object.vlan }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Status</th>
<td>
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
</td>
</tr>
<tr>
<th scope="row">Role</th>
<td>
{% if object.role %}
<a href="{{ object.role.get_absolute_url }}">{{ object.role }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Is a pool</th>
<td>{% checkmark object.is_pool %}</td>
</tr>
</table>
</div>
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Addressing</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Utilization</th>
<td>
{% if object.mark_utilized %}
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
<small>(Marked fully utilized)</small>
{% else %}
{% utilization_graph object.get_utilization %}
{% endif %}
</td>
</tr>
{% with child_ip_count=object.get_child_ips.count %}
<tr>
<th scope="row">Child IPs</th>
<td>
<a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">{{ child_ip_count }}</a>
</td>
</tr>
<tr>
<th scope="row">Available IPs</th>
<td>{{ object.get_available_ips|length }}</td>
</tr>
{% endwith %}
<tr>
<td>First available IP</td>
<td>
{% with first_available_ip=object.get_first_available_ip %}
{% if first_available_ip %}
{% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}">{{ first_available_ip }}</a>
{% else %}
{{ first_available_ip }}
{% endif %}
{% else %}
<span class="text-muted">None</span>
{% endif %}
{% endwith %}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">

View File

@ -30,13 +30,7 @@
</tr>
<tr>
<th scope="row">Private</th>
<td>
{% if object.is_private %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.is_private %}</td>
</tr>
<tr>
<th scope="row">Aggregates</th>

View File

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

View File

@ -30,13 +30,7 @@
</tr>
<tr>
<th scope="row">Unique IP Space</th>
<td>
{% if object.enforce_unique %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark object.enforce_unique %}</td>
</tr>
<tr>
<th scope="row">Description</th>

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="row">
<div class="col col-md-12">
<div class="col col-md-10 offset-md-1">
{% for token in tokens %}
<div class="card{% if token.is_expired %} bg-danger{% endif %}">
<div class="card-header">
@ -49,7 +49,8 @@
</div>
</div>
{% empty %}
<p>You do not have any API tokens.</p>
<h6><i class="mdi mdi-information"></i> You do not have any API tokens.</h6>
<p>Tokens are used to authenticate REST and GraphQL API requests.</p>
{% endfor %}
<div class="text-end">
<a href="{% url 'user:token_add' %}" class="btn btn-sm btn-primary my-3">

View File

@ -35,23 +35,11 @@
</tr>
<tr>
<th scope="row">Superuser</th>
<td>
{% if request.user.is_superuser %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark request.user.is_superuser %}</td>
</tr>
<tr>
<th scope="row">Admin Access</th>
<td>
{% if request.user.is_staff %}
<i class="mdi mdi-check-bold text-success" title="Yes"></i>
{% else %}
<i class="mdi mdi-close-thick text-danger" title="No"></i>
{% endif %}
</td>
<td>{% checkmark request.user.is_staff %}</td>
</tr>
</table>
</div>

View File

@ -5,7 +5,7 @@
{% block content %}
<form method="post">
{% csrf_token %}
{% include 'inc/table_controls_htmx.html' with table_modal="VMInterfaceTable_config" %}
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
<div class="card">
<div class="card-body" id="object_list">

View File

@ -22,8 +22,7 @@ class TenantGroupFilterForm(CustomFieldModelFilterForm):
parent_id = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
label=_('Parent group'),
fetch_trigger='open'
label=_('Parent group')
)
tag = TagFilterField(model)
@ -38,8 +37,7 @@ class TenantFilterForm(CustomFieldModelFilterForm):
queryset=TenantGroup.objects.all(),
required=False,
null_option='None',
label=_('Group'),
fetch_trigger='open'
label=_('Group')
)
tag = TagFilterField(model)
@ -53,8 +51,7 @@ class ContactGroupFilterForm(CustomFieldModelFilterForm):
parent_id = DynamicModelMultipleChoiceField(
queryset=ContactGroup.objects.all(),
required=False,
label=_('Parent group'),
fetch_trigger='open'
label=_('Parent group')
)
tag = TagFilterField(model)
@ -74,7 +71,6 @@ class ContactFilterForm(CustomFieldModelFilterForm):
queryset=ContactGroup.objects.all(),
required=False,
null_option='None',
label=_('Group'),
fetch_trigger='open'
label=_('Group')
)
tag = TagFilterField(model)

View File

@ -33,8 +33,7 @@ class TenancyFilterForm(forms.Form):
queryset=TenantGroup.objects.all(),
required=False,
null_option='None',
label=_('Tenant group'),
fetch_trigger='open'
label=_('Tenant group')
)
tenant_id = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
@ -43,6 +42,5 @@ class TenancyFilterForm(forms.Form):
query_params={
'group_id': '$tenant_group_id'
},
label=_('Tenant'),
fetch_trigger='open'
label=_('Tenant')
)

View File

@ -57,6 +57,7 @@ HTTP_REQUEST_META_SAFE_COPY = [
'HTTP_HOST',
'HTTP_REFERER',
'HTTP_USER_AGENT',
'HTTP_X_FORWARDED_FOR',
'QUERY_STRING',
'REMOTE_ADDR',
'REMOTE_HOST',

View File

@ -399,8 +399,8 @@ class DynamicModelChoiceMixin:
filter = django_filters.ModelChoiceFilter
widget = widgets.APISelect
def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, fetch_trigger=None,
empty_label=None, *args, **kwargs):
def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None,
fetch_trigger=None, empty_label=None, *args, **kwargs):
self.query_params = query_params or {}
self.initial_params = initial_params or {}
self.null_option = null_option

View File

@ -12,7 +12,7 @@ from django_tables2.data import TableQuerysetData
from django_tables2.utils import Accessor
from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField
from extras.models import CustomField, CustomLink
from .utils import content_type_identifier, content_type_name
from .paginator import EnhancedPaginator, get_paginate_count
@ -34,15 +34,18 @@ class BaseTable(tables.Table):
}
def __init__(self, *args, user=None, extra_columns=None, **kwargs):
if extra_columns is None:
extra_columns = []
# Add custom field columns
obj_type = ContentType.objects.get_for_model(self._meta.model)
cf_columns = [
(f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
]
if extra_columns is not None:
extra_columns.extend(cf_columns)
else:
extra_columns = cf_columns
cl_columns = [
(f'cl_{cl.name}', CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
]
extra_columns.extend([*cf_columns, *cl_columns])
super().__init__(*args, extra_columns=extra_columns, **kwargs)
@ -208,7 +211,6 @@ class ButtonsColumn(tables.TemplateColumn):
:param model: Model class to use for calculating URL view names
:param prepend_content: Additional template content to render in the column (optional)
:param return_url_extra: String to append to the return URL (e.g. for specifying a tab) (optional)
"""
buttons = ('changelog', 'edit', 'delete')
attrs = {'td': {'class': 'text-end text-nowrap noprint'}}
@ -220,18 +222,18 @@ class ButtonsColumn(tables.TemplateColumn):
</a>
{{% endif %}}
{{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}}
<a href="{{% url '{app_label}:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-sm btn-warning" title="Edit">
<a href="{{% url '{app_label}:{model_name}_edit' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-sm btn-warning" title="Edit">
<i class="mdi mdi-pencil"></i>
</a>
{{% endif %}}
{{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}}
<a href="{{% url '{app_label}:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}{{{{ return_url_extra }}}}" class="btn btn-sm btn-danger" title="Delete">
<a href="{{% url '{app_label}:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-sm btn-danger" title="Delete">
<i class="mdi mdi-trash-can-outline"></i>
</a>
{{% endif %}}
"""
def __init__(self, model, *args, buttons=None, prepend_template=None, return_url_extra='', **kwargs):
def __init__(self, model, *args, buttons=None, prepend_template=None, **kwargs):
if prepend_template:
prepend_template = prepend_template.replace('{', '{{')
prepend_template = prepend_template.replace('}', '}}')
@ -251,7 +253,6 @@ class ButtonsColumn(tables.TemplateColumn):
self.extra_context.update({
'buttons': buttons or self.buttons,
'return_url_extra': return_url_extra,
})
def header(self):
@ -420,6 +421,37 @@ class CustomFieldColumn(tables.Column):
return self.default
class CustomLinkColumn(tables.Column):
"""
Render a custom links as a table column.
"""
def __init__(self, customlink, *args, **kwargs):
self.customlink = customlink
kwargs['accessor'] = Accessor('pk')
if 'verbose_name' not in kwargs:
kwargs['verbose_name'] = customlink.name
super().__init__(*args, **kwargs)
def render(self, record):
try:
rendered = self.customlink.render({'obj': record})
if rendered:
return mark_safe(f'<a href="{rendered["link"]}"{rendered["link_target"]}>{rendered["text"]}</a>')
except Exception as e:
return mark_safe(f'<span class="text-danger" title="{e}"><i class="mdi mdi-alert"></i> Error</span>')
return ''
def value(self, record):
try:
rendered = self.customlink.render({'obj': record})
if rendered:
return rendered['link']
except Exception:
pass
return None
class MPTTColumn(tables.TemplateColumn):
"""
Display a nested hierarchy for MPTT-enabled models.

Some files were not shown because too many files have changed in this diff Show More