commit
1f575a2a47
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from netbox.config import PARAMS
|
|||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.1.2'
|
||||
VERSION = '3.1.3'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,38 +98,6 @@ type APISecret = {
|
|||
url: string;
|
||||
};
|
||||
|
||||
type JobResultLog = {
|
||||
message: string;
|
||||
status: 'success' | 'warning' | 'danger' | 'info';
|
||||
};
|
||||
|
||||
type JobStatus = {
|
||||
label: string;
|
||||
value: 'completed' | 'failed' | 'errored' | 'running';
|
||||
};
|
||||
|
||||
type APIJobResult = {
|
||||
completed: string;
|
||||
created: string;
|
||||
data: {
|
||||
log: JobResultLog[];
|
||||
output: string;
|
||||
};
|
||||
display: string;
|
||||
id: number;
|
||||
job_id: string;
|
||||
name: string;
|
||||
obj_type: string;
|
||||
status: JobStatus;
|
||||
url: string;
|
||||
user: {
|
||||
display: string;
|
||||
username: string;
|
||||
id: number;
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
|
||||
type APIUserConfig = {
|
||||
tables: { [k: string]: { columns: string[]; available_columns: string[] } };
|
||||
[k: string]: unknown;
|
||||
|
|
|
|||
|
|
@ -0,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export class APISelect {
|
|||
} else if (collapse !== null) {
|
||||
this.trigger = 'collapse';
|
||||
} else {
|
||||
this.trigger = 'load';
|
||||
this.trigger = 'open';
|
||||
}
|
||||
|
||||
switch (this.trigger) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ $theme-colors: map-merge(
|
|||
$theme-colors,
|
||||
(
|
||||
'primary': #337ab7,
|
||||
'info': #54d6f0,
|
||||
'red': $red-500,
|
||||
'yellow': $yellow-500,
|
||||
'green': $green-500,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% load nav %}
|
||||
{% load navigation %}
|
||||
{% load static %}
|
||||
|
||||
<nav class="sidenav noprint" id="sidenav" data-simplebar>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
|
||||
{% block title %}Swap Circuit Terminations{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete console port {{ consoleport }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete device bay {{ devicebay }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete interface {{ interface }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete inventory item {{ inventoryitem }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete power outlet {{ poweroutlet }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete power port {{ powerport }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Remove Virtual Chassis Member?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">—</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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">—</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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">—</span>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete {{ obj_type }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
<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>
|
||||
<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 %}
|
||||
|
|
|
|||
|
|
@ -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">—</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">—</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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue