Merge branch 'develop' into feature
This commit is contained in:
commit
ca59cd1eb8
|
|
@ -17,7 +17,7 @@ body:
|
|||
What version of NetBox are you currently running? (If you don't have access to the most
|
||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v3.0.5
|
||||
placeholder: v3.0.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ body:
|
|||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.0.5
|
||||
placeholder: v3.0.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ NetBox includes a `housekeeping` management command that should be run nightly.
|
|||
* Clearing expired authentication sessions from the database
|
||||
* Deleting changelog records older than the configured [retention time](../configuration/optional-settings.md#changelog_retention)
|
||||
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be copied into your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
|
||||
|
||||
The `housekeeping` command can also be run manually at any time: Running the command outside of scheduled execution times will not interfere with its operation.
|
||||
```shell
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
!!! note
|
||||
On Debian-based systems, be sure to omit the `.sh` file extension when linking to the script from within a cron directory. Otherwise, the task may not run.
|
||||
|
||||
The `housekeeping` command can also be run manually at any time: Running the command outside scheduled execution times will not interfere with its operation.
|
||||
|
|
|
|||
|
|
@ -259,10 +259,10 @@ python3 manage.py createsuperuser
|
|||
|
||||
NetBox includes a `housekeeping` management command that handles some recurring cleanup tasks, such as clearing out old sessions and expired change records. Although this command may be run manually, it is recommended to configure a scheduled job using the system's `cron` daemon or a similar utility.
|
||||
|
||||
A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
|
||||
A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to or linked from your system's daily cron task directory, or included within the crontab directly. (If installing NetBox into a nonstandard path, be sure to update the system paths within this script first.)
|
||||
|
||||
```shell
|
||||
cp /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
|
|
|||
|
|
@ -111,10 +111,10 @@ sudo systemctl restart netbox netbox-rq
|
|||
|
||||
## Verify Housekeeping Scheduling
|
||||
|
||||
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be copied to your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
|
||||
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be linked from your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
|
||||
|
||||
```shell
|
||||
cp /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/
|
||||
ln -s /opt/netbox/contrib/netbox-housekeeping.sh /etc/cron.daily/netbox-housekeeping
|
||||
```
|
||||
|
||||
See the [housekeeping documentation](../administration/housekeeping.md) for further details.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,24 @@
|
|||
# NetBox v3.0
|
||||
|
||||
## v3.0.6 (2021-10-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6850](https://github.com/netbox-community/netbox/issues/6850) - Default to current user when creating journal entries via REST API
|
||||
* [#6955](https://github.com/netbox-community/netbox/issues/6955) - Include type, ID, and slug on object view
|
||||
* [#7394](https://github.com/netbox-community/netbox/issues/7394) - Enable filtering cables by termination type & ID in REST API
|
||||
* [#7462](https://github.com/netbox-community/netbox/issues/7462) - Include count of assigned virtual machines under platform view
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#7442](https://github.com/netbox-community/netbox/issues/7442) - Fix missing actions column on user-configured tables
|
||||
* [#7446](https://github.com/netbox-community/netbox/issues/7446) - Fix exception when viewing a large number of child IPs within a prefix
|
||||
* [#7455](https://github.com/netbox-community/netbox/issues/7455) - Fix site/provider network validation for circuit termination API serializer
|
||||
* [#7459](https://github.com/netbox-community/netbox/issues/7459) - Pre-populate location data when adding a device to a rack
|
||||
* [#7460](https://github.com/netbox-community/netbox/issues/7460) - Fix filtering connections by site ID
|
||||
|
||||
---
|
||||
|
||||
## v3.0.5 (2021-10-04)
|
||||
|
||||
### Enhancements
|
||||
|
|
@ -8,7 +27,6 @@
|
|||
* [#6423](https://github.com/netbox-community/netbox/issues/6423) - Cache rendered REST API specifications
|
||||
* [#6708](https://github.com/netbox-community/netbox/issues/6708) - Add image attachment support for circuits, power panels
|
||||
* [#7387](https://github.com/netbox-community/netbox/issues/7387) - Enable arbitrary ordering of custom scripts
|
||||
* [#7427](https://github.com/netbox-community/netbox/issues/7427) - Don't select hidden rows when selecting all in a table
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
|
@ -23,6 +41,7 @@
|
|||
* [#7412](https://github.com/netbox-community/netbox/issues/7412) - Fix exception in UI when adding child device to device bay
|
||||
* [#7417](https://github.com/netbox-community/netbox/issues/7417) - Prevent exception when filtering objects list by invalid tag
|
||||
* [#7425](https://github.com/netbox-community/netbox/issues/7425) - Housekeeping command should honor zero verbosity
|
||||
* [#7427](https://github.com/netbox-community/netbox/issues/7427) - Don't select hidden rows when selecting all in a table
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ from rest_framework import serializers
|
|||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
|
||||
from dcim.api.serializers import CableTerminationSerializer
|
||||
from netbox.api import ChoiceField
|
||||
from netbox.api.serializers import (
|
||||
BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
|
||||
OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from .nested_serializers import *
|
||||
|
|
@ -90,11 +90,11 @@ class CircuitSerializer(PrimaryModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
|
||||
class CircuitTerminationSerializer(ValidatedModelSerializer, CableTerminationSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer(required=False)
|
||||
provider_network = NestedProviderNetworkSerializer(required=False)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -136,14 +136,20 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
|||
SIDE_A = CircuitTerminationSideChoices.SIDE_A
|
||||
SIDE_Z = CircuitTerminationSideChoices.SIDE_Z
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
provider_networks = (
|
||||
ProviderNetwork(provider=provider, name='Provider Network 1'),
|
||||
ProviderNetwork(provider=provider, name='Provider Network 2'),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuit_type),
|
||||
|
|
@ -153,10 +159,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
|||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side=SIDE_A),
|
||||
CircuitTermination(circuit=circuits[0], site=sites[1], term_side=SIDE_Z),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side=SIDE_A),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side=SIDE_Z),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
|
|
@ -164,13 +170,13 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
|||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_A,
|
||||
'site': sites[1].pk,
|
||||
'site': sites[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_Z,
|
||||
'site': sites[1].pk,
|
||||
'provider_network': provider_networks[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ from tenancy.filtersets import TenancyFilterSet
|
|||
from tenancy.models import Tenant
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CableFilterSet',
|
||||
'CableTerminationFilterSet',
|
||||
|
|
@ -1184,6 +1184,10 @@ class CableFilterSet(PrimaryModelFilterSet):
|
|||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
termination_a_type = ContentTypeFilter()
|
||||
termination_a_id = MultiValueNumberFilter()
|
||||
termination_b_type = ContentTypeFilter()
|
||||
termination_b_id = MultiValueNumberFilter()
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=CableTypeChoices
|
||||
)
|
||||
|
|
@ -1228,7 +1232,7 @@ class CableFilterSet(PrimaryModelFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'label', 'length', 'length_unit']
|
||||
fields = ['id', 'label', 'length', 'length_unit', 'termination_a_id', 'termination_b_id']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
|
@ -1243,73 +1247,6 @@ class CableFilterSet(PrimaryModelFilterSet):
|
|||
return queryset
|
||||
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(**{f'{name}__in': value})
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = []
|
||||
|
||||
|
||||
class PowerPanelFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
|
@ -1441,3 +1378,52 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
|
|||
Q(comments__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
#
|
||||
# Connection filter sets
|
||||
#
|
||||
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
site_id = MultiValueNumberFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__site_id'
|
||||
)
|
||||
site = MultiValueCharFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__site__slug'
|
||||
)
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_connections',
|
||||
field_name='device_id'
|
||||
)
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_connections',
|
||||
field_name='device__name'
|
||||
)
|
||||
|
||||
def filter_connections(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(**{f'{name}__in': value})
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = []
|
||||
|
|
|
|||
|
|
@ -132,14 +132,18 @@ class RackElevationSVG:
|
|||
|
||||
@staticmethod
|
||||
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
|
||||
link_url = '{}?{}'.format(
|
||||
reverse('dcim:device_add'),
|
||||
urlencode({
|
||||
'site': rack.site.pk,
|
||||
'location': rack.location.pk if rack.location else '',
|
||||
'rack': rack.pk,
|
||||
'face': face_id,
|
||||
'position': id_
|
||||
})
|
||||
)
|
||||
link = drawing.add(
|
||||
drawing.a(
|
||||
href='{}?{}'.format(
|
||||
reverse('dcim:device_add'),
|
||||
urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
|
||||
),
|
||||
target='_top'
|
||||
)
|
||||
drawing.a(href=link_url, target='_top')
|
||||
)
|
||||
if reservation:
|
||||
link.set_desc('{} — {} · {}'.format(
|
||||
|
|
|
|||
|
|
@ -2851,6 +2851,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
console_port = ConsolePort.objects.create(device=devices[0], name='Console Port 1')
|
||||
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
||||
|
||||
# Cables
|
||||
Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||
Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||
|
|
@ -2858,6 +2861,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||
Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||
Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['Cable 1', 'Cable 2']}
|
||||
|
|
@ -2877,7 +2881,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
|
||||
def test_status(self):
|
||||
params = {'status': [CableStatusChoices.STATUS_CONNECTED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'status': [CableStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
|
@ -2888,30 +2892,44 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
def test_device(self):
|
||||
devices = Device.objects.all()[:2]
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'device': [devices[0].name, devices[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_rack(self):
|
||||
racks = Rack.objects.all()[:2]
|
||||
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'rack': [racks[0].name, racks[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_site(self):
|
||||
site = Site.objects.all()[:2]
|
||||
params = {'site_id': [site[0].pk, site[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'site': [site[0].slug, site[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_tenant(self):
|
||||
tenant = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenant[0].pk, tenant[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
params = {'tenant': [tenant[0].slug, tenant[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
|
||||
def test_termination_types(self):
|
||||
params = {'termination_a_type': 'dcim.consoleport'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'termination_b_type': 'dcim.consoleserverport'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_termination_ids(self):
|
||||
interface_ids = Cable.objects.values_list('termination_a_id', flat=True)[:3]
|
||||
params = {
|
||||
'termination_a_type': 'dcim.interface',
|
||||
'termination_a_id': list(interface_ids),
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
|
|
|||
|
|
@ -1229,6 +1229,7 @@ class PlatformView(generic.ObjectView):
|
|||
|
||||
return {
|
||||
'devices_table': devices_table,
|
||||
'virtualmachine_count': VirtualMachine.objects.filter(platform=instance).count()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
|
|
@ -30,6 +31,7 @@ __all__ = (
|
|||
'ExportTemplateSerializer',
|
||||
'ImageAttachmentSerializer',
|
||||
'JobResultSerializer',
|
||||
'JournalEntrySerializer',
|
||||
'ObjectChangeSerializer',
|
||||
'ReportDetailSerializer',
|
||||
'ReportSerializer',
|
||||
|
|
@ -192,6 +194,12 @@ class JournalEntrySerializer(ValidatedModelSerializer):
|
|||
queryset=ContentType.objects.all()
|
||||
)
|
||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||
created_by = serializers.PrimaryKeyRelatedField(
|
||||
allow_null=True,
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
default=serializers.CurrentUserDefault()
|
||||
)
|
||||
kind = ChoiceField(
|
||||
choices=JournalEntryKindChoices,
|
||||
required=False
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
|||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.0.6-dev'
|
||||
VERSION = '3.0.7-dev'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -73,16 +73,6 @@
|
|||
color: color-contrast($value);
|
||||
}
|
||||
}
|
||||
|
||||
// Use proper foreground color in the alert body. Note: this is applied to p, & small because
|
||||
// we *don't* want to override the h1-h6 colors for alerts, since those are set to a color
|
||||
// similar to the alert color.
|
||||
.alert.alert-#{$color} {
|
||||
p,
|
||||
small {
|
||||
color: color-contrast($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure progress bars (utilization graph) in tables aren't too narrow to display the percentage.
|
||||
|
|
@ -200,16 +190,21 @@ div#advanced-search-content div.card div.card-body div.col:not(:last-child) {
|
|||
}
|
||||
|
||||
table {
|
||||
a {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
td {
|
||||
a {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.table > :not(caption) > * > * {
|
||||
padding-right: $table-cell-padding-x-sm !important;
|
||||
padding-left: $table-cell-padding-x-sm !important;
|
||||
th {
|
||||
a, a:hover {
|
||||
color: $body-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: $font-size-sm;
|
||||
|
|
@ -234,6 +229,11 @@ table {
|
|||
}
|
||||
}
|
||||
|
||||
&.table > :not(caption) > * > * {
|
||||
padding-right: $table-cell-padding-x-sm !important;
|
||||
padding-left: $table-cell-padding-x-sm !important;
|
||||
}
|
||||
|
||||
&.object-list {
|
||||
th {
|
||||
font-size: $font-size-xs;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ $spacing-s: $input-padding-x;
|
|||
span.arrow-down,
|
||||
span.arrow-up {
|
||||
border-color: currentColor;
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
// Don't show the depth indicator outside of the menu.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ $input-border-color: $gray-200;
|
|||
$theme-colors: map-merge(
|
||||
$theme-colors,
|
||||
(
|
||||
'primary': #337ab7,
|
||||
'red': $red-500,
|
||||
'yellow': $yellow-500,
|
||||
'green': $green-500,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
--nbx-color-mode-toggle-color: #{$primary};
|
||||
--nbx-sidenav-link-color: #{$gray-800};
|
||||
--nbx-sidenav-pin-color: #{$orange};
|
||||
--nbx-sidenav-parent-color: #{$gray-900};
|
||||
--nbx-sidenav-parent-color: #{$gray-800};
|
||||
--nbx-sidenav-group-color: #{$gray-800};
|
||||
|
||||
&[data-netbox-color-mode='dark'] {
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
--nbx-color-mode-toggle-color: #{$yellow-300};
|
||||
--nbx-sidenav-link-color: #{$gray-200};
|
||||
--nbx-sidenav-pin-color: #{$yellow};
|
||||
--nbx-sidenav-parent-color: #{$gray-100};
|
||||
--nbx-sidenav-parent-color: #{$gray-200};
|
||||
--nbx-sidenav-group-color: #{$gray-600};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
{% block title %}{{ obj.circuit.provider }} {{ obj.circuit }} - Side {{ form.term_side.value }}{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Circuit Termination</h5>
|
||||
</div>
|
||||
|
|
@ -53,9 +53,8 @@
|
|||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Termination Details</h5>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@
|
|||
<div class="row my-3">
|
||||
<div class="col col-md-5">
|
||||
<div class="card h-100">
|
||||
<h5 class="card-header">
|
||||
A Side
|
||||
</h5>
|
||||
<h5 class="card-header offset-sm-3">A Side</h5>
|
||||
<div class="card-body">
|
||||
{% if termination_a.device %}
|
||||
{# Device component #}
|
||||
|
|
@ -100,9 +98,7 @@
|
|||
</div>
|
||||
<div class="col col-md-5">
|
||||
<div class="card h-100">
|
||||
<h5 class="card-header">
|
||||
B Side
|
||||
</h5>
|
||||
<h5 class="card-header offset-sm-3">B Side</h5>
|
||||
<div class="card-body">
|
||||
{% if tabs %}
|
||||
<ul class="nav nav-tabs">
|
||||
|
|
@ -154,7 +150,7 @@
|
|||
<div class="row my-3 justify-content-center">
|
||||
<div class="col col-md-8">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Cable</h5>
|
||||
<h5 class="card-header offset-sm-3">Cable</h5>
|
||||
<div class="card-body">
|
||||
{% include 'dcim/inc/cable_form.html' %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,111 +4,104 @@
|
|||
{% block form %}
|
||||
{% render_errors form %}
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Device</h5>
|
||||
</div>
|
||||
{% render_field form.name %}
|
||||
{% render_field form.device_role %}
|
||||
{% render_field form.tags %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Device</h5>
|
||||
</div>
|
||||
{% render_field form.name %}
|
||||
{% render_field form.device_role %}
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Hardware</h5>
|
||||
</div>
|
||||
{% render_field form.manufacturer %}
|
||||
{% render_field form.device_type %}
|
||||
{% render_field form.serial %}
|
||||
{% render_field form.asset_tag %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Hardware</h5>
|
||||
</div>
|
||||
{% render_field form.manufacturer %}
|
||||
{% render_field form.device_type %}
|
||||
{% render_field form.serial %}
|
||||
{% render_field form.asset_tag %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Location</h5>
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Location</h5>
|
||||
</div>
|
||||
{% render_field form.region %}
|
||||
{% render_field form.site_group %}
|
||||
{% render_field form.site %}
|
||||
{% render_field form.location %}
|
||||
{% render_field form.rack %}
|
||||
|
||||
{% if obj.device_type.is_child_device and obj.parent_bay %}
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label">Parent Device</label>
|
||||
<div class="col">
|
||||
<input class="form-control" value="{{ obj.parent_bay.device }}" disabled />
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.region %}
|
||||
{% render_field form.site_group %}
|
||||
{% render_field form.site %}
|
||||
{% render_field form.location %}
|
||||
{% render_field form.rack %}
|
||||
|
||||
{% if obj.device_type.is_child_device and obj.parent_bay %}
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label">Parent Device</label>
|
||||
<div class="col">
|
||||
<input class="form-control" value="{{ obj.parent_bay.device }}" disabled />
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label">Parent Bay</label>
|
||||
<div class="col">
|
||||
<div class="input-group">
|
||||
<input class="form-control" value="{{ obj.parent_bay.name }}" disabled />
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" title="Regenerate Slug" class="btn btn-danger d-inline-flex align-items-center">
|
||||
<i class="mdi mdi-close-thick"></i> Remove
|
||||
</a>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label">Parent Bay</label>
|
||||
<div class="col">
|
||||
<div class="input-group">
|
||||
<input class="form-control" value="{{ obj.parent_bay.name }}" disabled />
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" title="Regenerate Slug" class="btn btn-danger d-inline-flex align-items-center">
|
||||
<i class="mdi mdi-close-thick"></i> Remove
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% render_field form.face %}
|
||||
{% render_field form.position %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% render_field form.face %}
|
||||
{% render_field form.position %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Management</h5>
|
||||
</div>
|
||||
{% render_field form.status %}
|
||||
{% render_field form.platform %}
|
||||
{% if obj.pk %}
|
||||
{% render_field form.primary_ip4 %}
|
||||
{% render_field form.primary_ip6 %}
|
||||
{% endif %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Management</h5>
|
||||
</div>
|
||||
{% render_field form.status %}
|
||||
{% render_field form.platform %}
|
||||
{% if obj.pk %}
|
||||
{% render_field form.primary_ip4 %}
|
||||
{% render_field form.primary_ip6 %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Virtualization</h5>
|
||||
</div>
|
||||
{% render_field form.cluster_group %}
|
||||
{% render_field form.cluster %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Virtualization</h5>
|
||||
</div>
|
||||
{% render_field form.cluster_group %}
|
||||
{% render_field form.cluster %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Tenancy</h5>
|
||||
</div>
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Tenancy</h5>
|
||||
</div>
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
<hr />
|
||||
{% endif %}
|
||||
|
||||
<div class="field-group my-4">
|
||||
<h5 class="text-center">Local Config Context Data</h5>
|
||||
{% render_field form.local_context_data %}
|
||||
<div class="field-group my-5">
|
||||
<h5 class="text-center">Local Config Context Data</h5>
|
||||
{% render_field form.local_context_data %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
{% render_field form.comments label='Comments' %}
|
||||
<div class="field-group mb-5">
|
||||
<h5 class="text-center">Comments</h5>
|
||||
{% render_field form.comments %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
</div>
|
||||
{% render_field form.tags %}
|
||||
{% if form.custom_fields %}
|
||||
<hr />
|
||||
<div class="field-group">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Interface</h5>
|
||||
</div>
|
||||
|
|
@ -27,9 +27,8 @@
|
|||
{% render_field form.mgmt_only %}
|
||||
{% render_field form.mark_connected %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">802.1Q Switching</h5>
|
||||
</div>
|
||||
|
|
@ -40,8 +39,7 @@
|
|||
</div>
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<hr />
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@
|
|||
<a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Virtual Machines</th>
|
||||
<td>
|
||||
<a href="{% url 'virtualization:virtualmachine_list' %}?platform_id={{ object.pk }}">{{ virtualmachine_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Rack</h5>
|
||||
</div>
|
||||
|
|
@ -15,9 +15,8 @@
|
|||
{% render_field form.role %}
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Inventory Control</h5>
|
||||
</div>
|
||||
|
|
@ -25,18 +24,16 @@
|
|||
{% render_field form.serial %}
|
||||
{% render_field form.asset_tag %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Tenancy</h5>
|
||||
</div>
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Dimensions</h5>
|
||||
</div>
|
||||
|
|
@ -45,34 +42,33 @@
|
|||
{% render_field form.u_height %}
|
||||
<div class="row mb-3">
|
||||
<label class="col col-md-3 col-form-label text-lg-end">Outer Dimensions</label>
|
||||
<div class="col col-md-3">
|
||||
<div class="col col-md-3 mb-1">
|
||||
{{ form.outer_width }}
|
||||
<div class="form-text">Width</div>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
<div class="col col-md-3 mb-1">
|
||||
{{ form.outer_depth }}
|
||||
<div class="form-text">Depth</div>
|
||||
</div>
|
||||
<div class="col col-md-3">
|
||||
<div class="col col-md-3 mb-1">
|
||||
{{ form.outer_unit }}
|
||||
<div class="form-text">Unit</div>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.desc_units %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
<hr />
|
||||
{% endif %}
|
||||
|
||||
<div class="field-group my-4">
|
||||
{% render_field form.comments label='Comments' %}
|
||||
<div class="field-group my-5">
|
||||
<h5 class="text-center">Comments</h5>
|
||||
{% render_field form.comments %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Virtual Chassis</h5>
|
||||
</div>
|
||||
|
|
@ -10,9 +10,8 @@
|
|||
{% render_field form.domain %}
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Member Devices</h5>
|
||||
</div>
|
||||
|
|
@ -25,8 +24,7 @@
|
|||
</div>
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<hr />
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
{% csrf_token %}
|
||||
{{ pk_form.pk }}
|
||||
{{ formset.management_form }}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Virtual Chassis</h5>
|
||||
</div>
|
||||
|
|
@ -20,16 +20,14 @@
|
|||
{% render_field vc_form.master %}
|
||||
{% render_field vc_form.tags %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
{% if vc_form.custom_fields %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
{% render_custom_fields vc_form %}
|
||||
</div>
|
||||
<hr />
|
||||
{% endif %}
|
||||
|
||||
<div class="field-group mb-5">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
{% block header %}
|
||||
{# Breadcrumbs #}
|
||||
<nav class="breadcrumb-container px-3" aria-label="breadcrumb">
|
||||
<div class="float-end">
|
||||
<code class="text-muted" title="Object type and ID">
|
||||
{{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}
|
||||
{% if object.slug %}({{ object.slug }}){% endif %}
|
||||
</code>
|
||||
</div>
|
||||
<ol class="breadcrumb">
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>
|
||||
|
|
|
|||
|
|
@ -6,18 +6,6 @@
|
|||
{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block controls %}
|
||||
{% if obj and settings.DOCS_ROOT %}
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<a href="{{ obj|get_docs_url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="View model documentation">
|
||||
<i class="mdi mdi-help-circle"></i> Help
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock controls %}
|
||||
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
<li class="nav-item" role="presentation">
|
||||
|
|
@ -31,6 +19,16 @@
|
|||
{% block content-wrapper %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab">
|
||||
|
||||
{# Link to model documentation #}
|
||||
{% if obj and settings.DOCS_ROOT %}
|
||||
<div class="float-end">
|
||||
<a href="{{ obj|get_docs_url }}" target="_blank" class="btn btn-sm btn-outline-secondary" title="View model documentation">
|
||||
<i class="mdi mdi-help-circle"></i> Help
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
|
|
@ -42,7 +40,7 @@
|
|||
|
||||
{# Render grouped fields according to Form #}
|
||||
{% for group, fields in form.Meta.fieldsets %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">{{ group }}</h5>
|
||||
</div>
|
||||
|
|
@ -50,14 +48,10 @@
|
|||
{% render_field form|getfield:name %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if not forloop.last %}
|
||||
<hr />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<hr />
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
|
|
@ -66,15 +60,15 @@
|
|||
{% endif %}
|
||||
|
||||
{% if form.comments %}
|
||||
<hr />
|
||||
<div class="field-group my-4">
|
||||
{% render_field form.comments label='Comments' %}
|
||||
<div class="field-group my-5">
|
||||
<h5 class="text-center">Comments</h5>
|
||||
{% render_field form.comments %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{# Render all fields in a single group #}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
{% block form_fields %}{% render_form form %}{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
|
||||
|
||||
{% block controls %}
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
|
|
@ -26,7 +28,7 @@
|
|||
{% block tab_items %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="object-list-tab" data-bs-toggle="tab" data-bs-target="#object-list" type="button" role="tab" aria-controls="edit-form" aria-selected="true">
|
||||
{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
|
||||
Records
|
||||
{% badge table.page.paginator.count %}
|
||||
</button>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
{% for section, items, icon in stats %}
|
||||
<div class="col col-sm-12 col-lg-6 col-xl-4 my-2 masonry-item">
|
||||
<div class="card">
|
||||
<h6 class="card-header text-primary text-center">
|
||||
<h6 class="card-header text-center">
|
||||
<i class="mdi mdi-{{ icon }}"></i>
|
||||
<span class="ms-1">{{ section }}</span>
|
||||
</h6>
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
<div class="row my-4 flex-grow-1 changelog-container">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<h6 class="card-header text-primary text-center">
|
||||
<h6 class="card-header text-center">
|
||||
<i class="mdi mdi-clipboard-clock"></i>
|
||||
<span class="ms-1">Change Log</span>
|
||||
</h6>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
<input
|
||||
type="text"
|
||||
class="form-control object-filter"
|
||||
placeholder="Filter"
|
||||
title="Filter text (regular expressions supported)"
|
||||
placeholder="Quick find"
|
||||
title="Find in the results below (regular expressions supported)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">IP Addresses</h5>
|
||||
</div>
|
||||
|
|
@ -20,9 +20,8 @@
|
|||
{% render_field model_form.description %}
|
||||
{% render_field model_form.tags %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Tenancy</h5>
|
||||
</div>
|
||||
|
|
@ -30,8 +29,7 @@
|
|||
{% render_field model_form.tenant %}
|
||||
</div>
|
||||
{% if model_form.custom_fields %}
|
||||
<hr />
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
{% endblock tabs %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">IP Address</h5>
|
||||
</div>
|
||||
|
|
@ -20,18 +20,16 @@
|
|||
{% render_field form.description %}
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Tenancy</h5>
|
||||
</div>
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Interface Assignment</h5>
|
||||
</div>
|
||||
|
|
@ -81,9 +79,8 @@
|
|||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">NAT IP (Inside)</h5>
|
||||
</div>
|
||||
|
|
@ -152,8 +149,7 @@
|
|||
</div>
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<hr />
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Service</h5>
|
||||
</div>
|
||||
|
|
@ -43,7 +43,6 @@
|
|||
</div>
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<hr />
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
{% load helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">VLAN</h5>
|
||||
</div>
|
||||
|
|
@ -15,18 +15,16 @@
|
|||
{% render_field form.description %}
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Tenancy</h5>
|
||||
</div>
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Assignment</h5>
|
||||
</div>
|
||||
|
|
@ -58,8 +56,7 @@
|
|||
</div>
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<hr />
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Sites</th>
|
||||
<th scope="row">Tenants</th>
|
||||
<td>
|
||||
<a href="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}">{{ tenants_table.rows|length }}</a>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Interface</h5>
|
||||
</div>
|
||||
|
|
@ -22,9 +22,8 @@
|
|||
{% render_field form.description %}
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">802.1Q Switching</h5>
|
||||
</div>
|
||||
|
|
@ -35,8 +34,7 @@
|
|||
</div>
|
||||
|
||||
{% if form.custom_fields %}
|
||||
<hr />
|
||||
<div class="field-group my-4">
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,14 +57,14 @@ class BaseTable(tables.Table):
|
|||
if user is not None and not isinstance(user, AnonymousUser):
|
||||
selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
|
||||
if selected_columns:
|
||||
pk = self.base_columns.pop('pk', None)
|
||||
actions = self.base_columns.pop('actions', None)
|
||||
|
||||
# Show only persistent or selected columns
|
||||
for name, column in self.columns.items():
|
||||
if name in selected_columns:
|
||||
if name in ['pk', 'actions', *selected_columns]:
|
||||
self.columns.show(name)
|
||||
else:
|
||||
self.columns.hide(name)
|
||||
|
||||
# Rearrange the sequence to list selected columns first, followed by all remaining columns
|
||||
# TODO: There's probably a more clever way to accomplish this
|
||||
self.sequence = [
|
||||
|
|
@ -72,12 +72,14 @@ class BaseTable(tables.Table):
|
|||
*[c for c in self.columns.names() if c not in selected_columns]
|
||||
]
|
||||
|
||||
# Always include PK and actions column, if defined on the table
|
||||
if pk:
|
||||
self.base_columns['pk'] = pk
|
||||
# PK column should always come first
|
||||
if 'pk' in self.sequence:
|
||||
self.sequence.remove('pk')
|
||||
self.sequence.insert(0, 'pk')
|
||||
if actions:
|
||||
self.base_columns['actions'] = actions
|
||||
|
||||
# Actions column should always come last
|
||||
if 'actions' in self.sequence:
|
||||
self.sequence.remove('actions')
|
||||
self.sequence.append('actions')
|
||||
|
||||
# Dynamically update the table's QuerySet to ensure related fields are pre-fetched
|
||||
|
|
@ -128,7 +130,7 @@ class BaseTable(tables.Table):
|
|||
prefixes/IP addresses/etc., where some table rows may represent available address space.
|
||||
"""
|
||||
if not hasattr(self, '_objects_count'):
|
||||
self._objects_count = sum(1 for obj in self.data if getattr(obj, 'pk'))
|
||||
self._objects_count = sum(1 for obj in self.data if hasattr(obj, 'pk'))
|
||||
return self._objects_count
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
Django==3.2.7
|
||||
django-cors-headers==3.9.0
|
||||
Django==3.2.8
|
||||
django-cors-headers==3.10.0
|
||||
django-debug-toolbar==3.2.2
|
||||
django-filter==21.1
|
||||
django-graphiql-debug-toolbar==0.2.0
|
||||
|
|
@ -8,14 +8,14 @@ django-pglocks==1.0.4
|
|||
django-prometheus==2.1.0
|
||||
django-redis==5.0.0
|
||||
django-rq==2.4.1
|
||||
django-tables2==2.4.0
|
||||
django-tables2==2.4.1
|
||||
django-taggit==1.5.1
|
||||
django-timezone-field==4.2.1
|
||||
djangorestframework==3.12.4
|
||||
drf-yasg[validation]==1.20.0
|
||||
graphene_django==2.15.0
|
||||
gunicorn==20.1.0
|
||||
Jinja2==3.0.1
|
||||
Jinja2==3.0.2
|
||||
Markdown==3.3.4
|
||||
markdown-include==0.6.0
|
||||
mkdocs-material==7.3.1
|
||||
|
|
|
|||
Loading…
Reference in New Issue