Merge pull request #8185 from netbox-community/develop

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,31 @@
# NetBox v3.1 # 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) ## v3.1.2 (2021-12-20)
### Enhancements ### Enhancements

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,13 +27,7 @@ from virtualization.models import VirtualMachine
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import DeviceFaceChoices from .choices import DeviceFaceChoices
from .constants import NONCONNECTABLE_IFACE_TYPES from .constants import NONCONNECTABLE_IFACE_TYPES
from .models import ( 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,
)
class DeviceComponentsView(generic.ObjectChildrenView): class DeviceComponentsView(generic.ObjectChildrenView):
@ -51,10 +45,21 @@ class DeviceComponentsView(generic.ObjectChildrenView):
class DeviceTypeComponentsView(DeviceComponentsView): class DeviceTypeComponentsView(DeviceComponentsView):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
template_name = 'dcim/devicetype/component_templates.html' template_name = 'dcim/devicetype/component_templates.html'
viewname = None # Used for return_url resolution
def get_children(self, request, parent): def get_children(self, request, parent):
return self.child_model.objects.restrict(request.user, 'view').filter(device_type=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): class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
""" """
@ -798,48 +803,56 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
child_model = ConsolePortTemplate child_model = ConsolePortTemplate
table = tables.ConsolePortTemplateTable table = tables.ConsolePortTemplateTable
filterset = filtersets.ConsolePortTemplateFilterSet filterset = filtersets.ConsolePortTemplateFilterSet
viewname = 'dcim:devicetype_consoleports'
class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView): class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
child_model = ConsoleServerPortTemplate child_model = ConsoleServerPortTemplate
table = tables.ConsoleServerPortTemplateTable table = tables.ConsoleServerPortTemplateTable
filterset = filtersets.ConsoleServerPortTemplateFilterSet filterset = filtersets.ConsoleServerPortTemplateFilterSet
viewname = 'dcim:devicetype_consoleserverports'
class DeviceTypePowerPortsView(DeviceTypeComponentsView): class DeviceTypePowerPortsView(DeviceTypeComponentsView):
child_model = PowerPortTemplate child_model = PowerPortTemplate
table = tables.PowerPortTemplateTable table = tables.PowerPortTemplateTable
filterset = filtersets.PowerPortTemplateFilterSet filterset = filtersets.PowerPortTemplateFilterSet
viewname = 'dcim:devicetype_powerports'
class DeviceTypePowerOutletsView(DeviceTypeComponentsView): class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
child_model = PowerOutletTemplate child_model = PowerOutletTemplate
table = tables.PowerOutletTemplateTable table = tables.PowerOutletTemplateTable
filterset = filtersets.PowerOutletTemplateFilterSet filterset = filtersets.PowerOutletTemplateFilterSet
viewname = 'dcim:devicetype_poweroutlets'
class DeviceTypeInterfacesView(DeviceTypeComponentsView): class DeviceTypeInterfacesView(DeviceTypeComponentsView):
child_model = InterfaceTemplate child_model = InterfaceTemplate
table = tables.InterfaceTemplateTable table = tables.InterfaceTemplateTable
filterset = filtersets.InterfaceTemplateFilterSet filterset = filtersets.InterfaceTemplateFilterSet
viewname = 'dcim:devicetype_interfaces'
class DeviceTypeFrontPortsView(DeviceTypeComponentsView): class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
child_model = FrontPortTemplate child_model = FrontPortTemplate
table = tables.FrontPortTemplateTable table = tables.FrontPortTemplateTable
filterset = filtersets.FrontPortTemplateFilterSet filterset = filtersets.FrontPortTemplateFilterSet
viewname = 'dcim:devicetype_frontports'
class DeviceTypeRearPortsView(DeviceTypeComponentsView): class DeviceTypeRearPortsView(DeviceTypeComponentsView):
child_model = RearPortTemplate child_model = RearPortTemplate
table = tables.RearPortTemplateTable table = tables.RearPortTemplateTable
filterset = filtersets.RearPortTemplateFilterSet filterset = filtersets.RearPortTemplateFilterSet
viewname = 'dcim:devicetype_rearports'
class DeviceTypeDeviceBaysView(DeviceTypeComponentsView): class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
child_model = DeviceBayTemplate child_model = DeviceBayTemplate
table = tables.DeviceBayTemplateTable table = tables.DeviceBayTemplateTable
filterset = filtersets.DeviceBayTemplateFilterSet filterset = filtersets.DeviceBayTemplateFilterSet
viewname = 'dcim:devicetype_devicebays'
class DeviceTypeEditView(generic.ObjectEditView): class DeviceTypeEditView(generic.ObjectEditView):

View File

@ -5,10 +5,10 @@ from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers from rest_framework import serializers
from dcim.api.nested_serializers import ( from dcim.api.nested_serializers import (
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedRegionSerializer,
NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, 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.choices import *
from extras.models import * from extras.models import *
from extras.utils import FeatureQuery from extras.utils import FeatureQuery

View File

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

View File

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

View File

@ -229,6 +229,24 @@ class CustomLink(ChangeLoggedModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('extras:customlink', args=[self.pk]) 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') @extras_features('webhooks', 'export_templates')
class ExportTemplate(ChangeLoggedModel): class ExportTemplate(ChangeLoggedModel):

View File

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

View File

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

View File

@ -10,6 +10,7 @@ from rq import Worker
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.htmx import is_htmx
from utilities.tables import paginate_table from utilities.tables import paginate_table
from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict from utilities.utils import copy_safe_request, count_related, normalize_querydict, shallow_compare_dict
from utilities.views import ContentTypePermissionRequiredMixin from utilities.views import ContentTypePermissionRequiredMixin
@ -471,6 +472,7 @@ class ObjectChangeLogView(View):
class ImageAttachmentEditView(generic.ObjectEditView): class ImageAttachmentEditView(generic.ObjectEditView):
queryset = ImageAttachment.objects.all() queryset = ImageAttachment.objects.all()
model_form = forms.ImageAttachmentForm model_form = forms.ImageAttachmentForm
template_name = 'extras/imageattachment_edit.html'
def alter_obj(self, instance, request, args, kwargs): def alter_obj(self, instance, request, args, kwargs):
if not instance.pk: if not instance.pk:
@ -693,16 +695,26 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View):
def get(self, request, job_result_pk): def get(self, request, job_result_pk):
report_content_type = ContentType.objects.get(app_label='extras', model='report') 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 # 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 = 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', { return render(request, 'extras/report_result.html', {
'report': report, 'report': report,
'result': jobresult, 'result': result,
}) })
@ -820,6 +832,16 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View)
script = self._get_script(result.name) 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', { return render(request, 'extras/script_result.html', {
'script': script, 'script': script,
'result': result, 'result': result,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -98,38 +98,6 @@ type APISecret = {
url: string; 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 = { type APIUserConfig = {
tables: { [k: string]: { columns: string[]; available_columns: string[] } }; tables: { [k: string]: { columns: string[]; available_columns: string[] } };
[k: string]: unknown; [k: string]: unknown;

View File

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

View File

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

View File

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

View File

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

View File

@ -965,6 +965,19 @@ div.card-overlay {
max-width: unset; 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 // Preformatted text blocks
td pre { td pre {
margin-bottom: 0 margin-bottom: 0

View File

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

View File

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

View File

@ -1,8 +1,7 @@
{# Base layout for the core NetBox UI w/navbar and page content #} {# Base layout for the core NetBox UI w/navbar and page content #}
{% extends 'base/base.html' %} {% extends 'base/base.html' %}
{% load helpers %} {% load helpers %}
{% load nav %} {% load search %}
{% load search_options %}
{% load static %} {% load static %}
{% block layout %} {% block layout %}
@ -21,7 +20,7 @@
</div> </div>
{# Top bar #} {# 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 #} {# Mobile Navigation #}
<div class="nav-mobile"> <div class="nav-mobile">

View File

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

View File

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

View File

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

View File

@ -41,11 +41,11 @@
</tr> </tr>
<tr> <tr>
<th scope="row">NOC Contact</th> <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>
<tr> <tr>
<th scope="row">Admin Contact</th> <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>
<tr> <tr>
<th scope="row">Circuits</th> <th scope="row">Circuits</th>

View File

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

View File

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

View File

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

View File

@ -95,74 +95,74 @@
</a> </a>
</li> </li>
{% with interface_count=object.interfaces_count %} {% with tab_name='interfaces' interface_count=object.interfaces_count %}
{% if interface_count %} {% if active_tab == tab_name or interface_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with frontport_count=object.frontports.count %} {% with tab_name='front-ports' frontport_count=object.frontports.count %}
{% if frontport_count %} {% if active_tab == tab_name or frontport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with rearport_count=object.rearports.count %} {% with tab_name='rear-ports' rearport_count=object.rearports.count %}
{% if rearport_count %} {% if active_tab == tab_name or rearport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with consoleport_count=object.consoleports.count %} {% with tab_name='console-ports' consoleport_count=object.consoleports.count %}
{% if consoleport_count %} {% if active_tab == tab_name or consoleport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with consoleserverport_count=object.consoleserverports.count %} {% with tab_name='console-server-ports' consoleserverport_count=object.consoleserverports.count %}
{% if consoleserverport_count %} {% if active_tab == tab_name or consoleserverport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with powerport_count=object.powerports.count %} {% with tab_name='power-ports' powerport_count=object.powerports.count %}
{% if powerport_count %} {% if active_tab == tab_name or powerport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with poweroutlet_count=object.poweroutlets.count %} {% with tab_name='power-outlets' poweroutlet_count=object.poweroutlets.count %}
{% if poweroutlet_count %} {% if active_tab == tab_name or poweroutlet_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with devicebay_count=object.devicebays.count %} {% with tab_name='device-bays' devicebay_count=object.devicebays.count %}
{% if devicebay_count %} {% if active_tab == tab_name or devicebay_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with inventoryitem_count=object.inventoryitems.count %} {% with tab_name='inventory-items' inventoryitem_count=object.inventoryitems.count %}
{% if inventoryitem_count %} {% if active_tab == tab_name or inventoryitem_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}

View File

@ -17,22 +17,22 @@
<div class="noprint bulk-buttons"> <div class="noprint bulk-buttons">
<div class="bulk-button-group"> <div class="bulk-button-group">
{% if perms.dcim.change_devicebay %} {% 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 <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button> </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 <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.delete_devicebay %} {% 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 <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
</button> </button>
{% endif %} {% endif %}
</div> </div>
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<div class="bulk-button-group"> <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 <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
</a> </a>
</div> </div>

View File

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

View File

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

View File

@ -40,13 +40,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">VM Role</th> <th scope="row">VM Role</th>
<td> <td>{% checkmark object.vm_role %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Devices</th> <th scope="row">Devices</th>

View File

@ -33,13 +33,7 @@
</tr> </tr>
<tr> <tr>
<td>Full Depth</td> <td>Full Depth</td>
<td> <td>{% checkmark object.is_full_depth %}</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>
</tr> </tr>
<tr> <tr>
<td>Parent/Child</td> <td>Parent/Child</td>

View File

@ -18,28 +18,28 @@
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if perms.dcim.add_consoleporttemplate %} {% 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 %} {% endif %}
{% if perms.dcim.add_consoleserverporttemplate %} {% 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 %} {% endif %}
{% if perms.dcim.add_powerporttemplate %} {% 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 %} {% endif %}
{% if perms.dcim.add_poweroutlettemplate %} {% 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 %} {% endif %}
{% if perms.dcim.add_interfacetemplate %} {% 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 %} {% endif %}
{% if perms.dcim.add_frontporttemplate %} {% 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 %} {% endif %}
{% if perms.dcim.add_rearporttemplate %} {% 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 %} {% endif %}
{% if perms.dcim.add_devicebaytemplate %} {% 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 %} {% endif %}
</ul> </ul>
</div> </div>
@ -53,66 +53,66 @@
</a> </a>
</li> </li>
{% with interface_count=object.interfacetemplates.count %} {% with tab_name='interface-templates' interface_count=object.interfacetemplates.count %}
{% if interface_count %} {% if active_tab == tab_name or interface_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with frontport_count=object.frontporttemplates.count %} {% with tab_name='front-port-templates' frontport_count=object.frontporttemplates.count %}
{% if frontport_count %} {% if active_tab == tab_name or frontport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with rearport_count=object.rearporttemplates.count %} {% with tab_name='rear-port-templates' rearport_count=object.rearporttemplates.count %}
{% if rearport_count %} {% if active_tab == tab_name or rearport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with consoleport_count=object.consoleporttemplates.count %} {% with tab_name='console-port-templates' consoleport_count=object.consoleporttemplates.count %}
{% if consoleport_count %} {% if active_tab == tab_name or consoleport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with consoleserverport_count=object.consoleserverporttemplates.count %} {% with tab_name='console-server-port-templates' consoleserverport_count=object.consoleserverporttemplates.count %}
{% if consoleserverport_count %} {% if active_tab == tab_name or consoleserverport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with powerport_count=object.powerporttemplates.count %} {% with tab_name='power-port-templates' powerport_count=object.powerporttemplates.count %}
{% if powerport_count %} {% if active_tab == tab_name or powerport_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with poweroutlet_count=object.poweroutlettemplates.count %} {% with tab_name='power-outlet-templates' poweroutlet_count=object.poweroutlettemplates.count %}
{% if poweroutlet_count %} {% if active_tab == tab_name or poweroutlet_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% with devicebay_count=object.devicebaytemplates.count %} {% with tab_name='device-bay-templates' devicebay_count=object.devicebaytemplates.count %}
{% if devicebay_count %} {% if active_tab == tab_name or devicebay_count %}
<li role="presentation" class="nav-item"> <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> </li>
{% endif %} {% endif %}
{% endwith %} {% endwith %}

View File

@ -13,18 +13,18 @@
</div> </div>
<div class="card-footer noprint"> <div class="card-footer noprint">
{% if table.rows %} {% 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 <span class="mdi mdi-pencil-outline" aria-hidden="true"></span> Rename
</button> </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 <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
</button> </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 <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
</button> </button>
{% endif %} {% endif %}
<div class="float-end"> <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> <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
Add {{ title }} Add {{ title }}
</a> </a>

View File

@ -48,23 +48,11 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Enabled</th> <th scope="row">Enabled</th>
<td> <td>{% checkmark object.enabled %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Management Only</th> <th scope="row">Management Only</th>
<td> <td>{% checkmark object.mgmt_only %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Parent</th> <th scope="row">Parent</th>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,13 +29,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Required</th> <th scope="row">Required</th>
<td> <td>{% checkmark object.required %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Weight</th> <th scope="row">Weight</th>

View File

@ -33,13 +33,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">New Window</th> <th scope="row">New Window</th>
<td> <td>{% checkmark object.new_window %}</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>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -40,13 +40,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Attachment</th> <th scope="row">Attachment</th>
<td> <td>{% checkmark object.as_attachment %}</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>
</tr> </tr>
</table> </table>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,99 +1,9 @@
{% extends 'extras/report.html' %} {% 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 %} {% block content-wrapper %}
<div class="row px-3"> <div class="row px-3">
<div class="col col-md-12"> <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 %}>
<p> {% include 'extras/htmx/report_result.html' %}
Run: <strong>{{ result.created|annotated_date }}</strong>
{% if result.completed %}
Duration: <strong>{{ result.duration }}</strong>
{% else %}
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
{% endif %}
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
</p>
{% if result.completed %}
<div class="card">
<h5 class="card-header">
Report Methods
</h5>
<div class="card-body">
<table class="table table-hover">
{% for method, data in result.data.items %}
<tr>
<td class="font-monospace"><a href="#{{ method }}">{{ method }}</a></td>
<td class="text-end report-stats">
<span class="badge bg-success">{{ data.success }}</span>
<span class="badge bg-info">{{ data.info }}</span>
<span class="badge bg-warning">{{ data.warning }}</span>
<span class="badge bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card">
<h5 class="card-header">
Report Results
</h5>
<div class="card-body">
<table class="table table-hover report">
<thead>
<tr class="table-headings">
<th>Time</th>
<th>Level</th>
<th>Object</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{% for method, data in result.data.items %}
<tr>
<th colspan="4" style="font-family: monospace">
<a name="{{ method }}"></a>{{ method }}
</th>
</tr>
{% for time, level, obj, url, message in data.log %}
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
<td>{{ time }}</td>
<td>
<label class="badge bg-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
</td>
<td>
{% if obj and url %}
<a href="{{ url }}">{{ obj }}</a>
{% elif obj %}
{{ obj }}
{% else %}
<span class="muted">&mdash;</span>
{% endif %}
</td>
<td class="rendered-markdown">{{ message|render_markdown }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="well">Pending results</div>
{% endif %}
</div> </div>
</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>
{% endblock %} {% endblock %}

View File

@ -1,117 +1,48 @@
{% extends 'base/layout.html' %} {% extends 'base/layout.html' %}
{% load helpers %} {% 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 title %}{{ script }}{% endblock %}
{% block subtitle %} {% block subtitle %}
{{ script.Meta.description|render_markdown }} {{ script.Meta.description|render_markdown }}
<span id="pending-result-label">{% include 'extras/inc/job_label.html' with result=result %}</span>
{% endblock %} {% endblock %}
{% block header %} {% block header %}
<div class="row noprint"> <div class="row noprint">
<div class="col col-md-12"> <div class="col col-md-12">
<nav class="breadcrumb-container px-3" aria-label="breadcrumb"> <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
<ol class="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' %}">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_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"><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
<li class="breadcrumb-item">{{ result.created|annotated_date }}</li> <li class="breadcrumb-item">{{ result.created|annotated_date }}</li>
</ol> </ol>
</nav> </nav>
</div> </div>
</div> </div>
{{ block.super }} {{ block.super }}
{% endblock header %} {% endblock header %}
{% block content-wrapper %} {% block content-wrapper %}
<ul class="nav nav-tabs px-3" role="tablist"> <ul class="nav nav-tabs px-3" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">Log</a> <a href="#log" role="tab" data-bs-toggle="tab" class="nav-link active">Log</a>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a href="#output" role="tab" data-bs-toggle="tab" class="nav-link">Output</a> <a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
</li> </li>
<li class="nav-item" role="presentation">
<a href="#source" role="tab" data-bs-toggle="tab" class="nav-link">Source</a>
</li>
</ul> </ul>
<div class="tab-content mb-3"> <div class="tab-content mb-3">
<p> <div role="tabpanel" class="tab-pane active" id="log">
Run: <strong>{{ result.created|annotated_date }}</strong> <div class="row">
{% if result.completed %} <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 %}>
Duration: <strong>{{ result.duration }}</strong> {% include 'extras/htmx/script_result.html' %}
{% else %} </div>
<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> </div>
</div>
<div role="tabpanel" class="tab-pane" id="source">
<p><code>{{ script.filename }}</code></p>
<pre class="block">{{ script.source }}</pre>
</div>
</div> </div>
{% endblock content-wrapper %} {% endblock content-wrapper %}
{% block data %}
<span data-job-url="{% url 'extras-api:jobresult-detail' pk=result.pk %}"></span>
<span data-job-complete="{{ result.completed }}"></span>
{% endblock %}

View File

@ -17,13 +17,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Enabled</th> <th scope="row">Enabled</th>
<td> <td>{% checkmark object.enabled %}</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>
</tr> </tr>
</table> </table>
</div> </div>
@ -36,33 +30,15 @@
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Create</th> <th scope="row">Create</th>
<td> <td>{% checkmark object.type_create %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Update</th> <th scope="row">Update</th>
<td> <td>{% checkmark object.type_update %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Delete</th> <th scope="row">Delete</th>
<td> <td>{% checkmark object.type_delete %}</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>
</tr> </tr>
</table> </table>
</div> </div>
@ -100,13 +76,7 @@
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">SSL Verification</th> <th scope="row">SSL Verification</th>
<td> <td>{% checkmark object.ssl_verification %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">CA File Path</th> <th scope="row">CA File Path</th>

View File

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

View File

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

View File

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

View File

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

View File

@ -15,9 +15,9 @@
{% if field.type == 'longtext' and value %} {% if field.type == 'longtext' and value %}
{{ value|render_markdown }} {{ value|render_markdown }}
{% elif field.type == 'boolean' and value == True %} {% 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 %} {% 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 %} {% elif field.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a> <a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif field.type == 'json' and value %} {% elif field.type == 'json' and value %}

View File

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

View File

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

View File

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

View File

@ -4,127 +4,154 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Prefix</h5>
Prefix <div class="card-body">
</h5> <table class="table table-hover attr-table">
<div class="card-body"> <tr>
<table class="table table-hover attr-table"> <th scope="row">Family</th>
<tr> <td>IPv{{ object.family }}</td>
<th scope="row">Family</th> </tr>
<td>IPv{{ object.family }}</td> <tr>
</tr> <th scope="row">VRF</th>
<tr> <td>
<th scope="row">VRF</th> {% if object.vrf %}
<td> <a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a> ({{ object.vrf.rd }})
{% if object.vrf %} {% else %}
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a> ({{ object.vrf.rd }}) <span>Global</span>
{% else %} {% endif %}
<span>Global</span> </td>
{% endif %} </tr>
</td> <tr>
</tr> <th scope="row">Tenant</th>
<tr> <td>
<th scope="row">Tenant</th> {% if object.tenant %}
<td> {% if object.tenant.group %}
{% if object.tenant %} <a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
{% if object.tenant.group %} {% endif %}
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> / <a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
{% endif %} {% else %}
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a> <span class="text-muted">None</span>
{% else %} {% endif %}
<span class="text-muted">None</span> </td>
{% endif %} </tr>
</td> <tr>
</tr> <th scope="row">Aggregate</th>
<tr> <td>
<th scope="row">Aggregate</th> {% if aggregate %}
<td> <a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate.prefix }}</a> ({{ aggregate.rir }})
{% if aggregate %} {% else %}
<a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate.prefix }}</a> ({{ aggregate.rir }}) <span class="text-warning">None</span>
{% else %} {% endif %}
<span class="text-warning">None</span> </td>
{% endif %} </tr>
</td> <tr>
</tr> <th scope="row">Site</th>
<tr> <td>
<th scope="row">Site</th> {% if object.site %}
<td> {% if object.site.region %}
{% if object.site %} <a href="{{ object.site.region.get_absolute_url }}">{{ object.site.region }}</a> /
{% if object.site.region %} {% endif %}
<a href="{{ object.site.region.get_absolute_url }}">{{ object.site.region }}</a> / <a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
{% endif %} {% else %}
<a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a> <span class="text-muted">None</span>
{% else %} {% endif %}
<span class="text-muted">None</span> </td>
{% endif %} </tr>
</td> <tr>
</tr> <th scope="row">VLAN</th>
<tr> <td>
<th scope="row">VLAN</th> {% if object.vlan %}
<td> {% if object.vlan.group %}
{% if object.vlan %} <a href="{{ object.vlan.group.get_absolute_url }}">{{ object.vlan.group }}</a> /
{% if object.vlan.group %} {% endif %}
<a href="{{ object.vlan.group.get_absolute_url }}">{{ object.vlan.group }}</a> / <a href="{% url 'ipam:vlan' pk=object.vlan.pk %}">{{ object.vlan }}</a>
{% endif %} {% else %}
<a href="{% url 'ipam:vlan' pk=object.vlan.pk %}">{{ object.vlan }}</a> <span class="text-muted">None</span>
{% else %} {% endif %}
<span class="text-muted">None</span> </td>
{% endif %} </tr>
</td> <tr>
</tr> <th scope="row">Status</th>
<tr> <td>
<th scope="row">Status</th> <span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
<td> </td>
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span> </tr>
</td> <tr>
</tr> <th scope="row">Role</th>
<tr> <td>
<th scope="row">Role</th> {% if object.role %}
<td> <a href="{{ object.role.get_absolute_url }}">{{ object.role }}</a>
{% if object.role %} {% else %}
<a href="{{ object.role.get_absolute_url }}">{{ object.role }}</a> <span class="text-muted">None</span>
{% else %} {% endif %}
<span class="text-muted">None</span> </td>
{% endif %} </tr>
</td> <tr>
</tr> <th scope="row">Description</th>
<tr> <td>{{ object.description|placeholder }}</td>
<th scope="row">Description</th> </tr>
<td>{{ object.description|placeholder }}</td> <tr>
</tr> <th scope="row">Is a pool</th>
<tr> <td>{% checkmark object.is_pool %}</td>
<th scope="row">Is a pool</th> </tr>
<td> </table>
{% if object.is_pool %} </div>
<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> </div>
<div class="col col-md-6"> {% plugin_left_page object %}
{% include 'inc/panels/custom_fields.html' %} </div>
{% include 'inc/panels/tags.html' %} <div class="col col-md-6">
{% plugin_right_page object %} <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> </div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% plugin_right_page object %}
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">

View File

@ -30,13 +30,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Private</th> <th scope="row">Private</th>
<td> <td>{% checkmark object.is_private %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Aggregates</th> <th scope="row">Aggregates</th>

View File

@ -24,8 +24,6 @@
</div> </div>
</div> </div>
<div class="tab-content p-0 border-0"> <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"> <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 %} {% render_field form.device %}
</div> </div>

View File

@ -30,13 +30,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Unique IP Space</th> <th scope="row">Unique IP Space</th>
<td> <td>{% checkmark object.enforce_unique %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">Description</th>

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-10 offset-md-1">
{% for token in tokens %} {% for token in tokens %}
<div class="card{% if token.is_expired %} bg-danger{% endif %}"> <div class="card{% if token.is_expired %} bg-danger{% endif %}">
<div class="card-header"> <div class="card-header">
@ -49,7 +49,8 @@
</div> </div>
</div> </div>
{% empty %} {% 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 %} {% endfor %}
<div class="text-end"> <div class="text-end">
<a href="{% url 'user:token_add' %}" class="btn btn-sm btn-primary my-3"> <a href="{% url 'user:token_add' %}" class="btn btn-sm btn-primary my-3">

View File

@ -35,23 +35,11 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Superuser</th> <th scope="row">Superuser</th>
<td> <td>{% checkmark request.user.is_superuser %}</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>
</tr> </tr>
<tr> <tr>
<th scope="row">Admin Access</th> <th scope="row">Admin Access</th>
<td> <td>{% checkmark request.user.is_staff %}</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>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<form method="post"> <form method="post">
{% csrf_token %} {% 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">
<div class="card-body" id="object_list"> <div class="card-body" id="object_list">

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ from django_tables2.data import TableQuerysetData
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from extras.choices import CustomFieldTypeChoices 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 .utils import content_type_identifier, content_type_name
from .paginator import EnhancedPaginator, get_paginate_count 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): def __init__(self, *args, user=None, extra_columns=None, **kwargs):
if extra_columns is None:
extra_columns = []
# Add custom field columns # Add custom field columns
obj_type = ContentType.objects.get_for_model(self._meta.model) obj_type = ContentType.objects.get_for_model(self._meta.model)
cf_columns = [ cf_columns = [
(f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type) (f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
] ]
if extra_columns is not None: cl_columns = [
extra_columns.extend(cf_columns) (f'cl_{cl.name}', CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
else: ]
extra_columns = cf_columns extra_columns.extend([*cf_columns, *cl_columns])
super().__init__(*args, extra_columns=extra_columns, **kwargs) 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 model: Model class to use for calculating URL view names
:param prepend_content: Additional template content to render in the column (optional) :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') buttons = ('changelog', 'edit', 'delete')
attrs = {'td': {'class': 'text-end text-nowrap noprint'}} attrs = {'td': {'class': 'text-end text-nowrap noprint'}}
@ -220,18 +222,18 @@ class ButtonsColumn(tables.TemplateColumn):
</a> </a>
{{% endif %}} {{% endif %}}
{{% if "edit" in buttons and perms.{app_label}.change_{model_name} %}} {{% 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> <i class="mdi mdi-pencil"></i>
</a> </a>
{{% endif %}} {{% endif %}}
{{% if "delete" in buttons and perms.{app_label}.delete_{model_name} %}} {{% 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> <i class="mdi mdi-trash-can-outline"></i>
</a> </a>
{{% endif %}} {{% 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: if prepend_template:
prepend_template = prepend_template.replace('{', '{{') prepend_template = prepend_template.replace('{', '{{')
prepend_template = prepend_template.replace('}', '}}') prepend_template = prepend_template.replace('}', '}}')
@ -251,7 +253,6 @@ class ButtonsColumn(tables.TemplateColumn):
self.extra_context.update({ self.extra_context.update({
'buttons': buttons or self.buttons, 'buttons': buttons or self.buttons,
'return_url_extra': return_url_extra,
}) })
def header(self): def header(self):
@ -420,6 +421,37 @@ class CustomFieldColumn(tables.Column):
return self.default 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): class MPTTColumn(tables.TemplateColumn):
""" """
Display a nested hierarchy for MPTT-enabled models. Display a nested hierarchy for MPTT-enabled models.

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