mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
ca59cd1eb8
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -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
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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()
|
||||
|
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-print.css
vendored
BIN
netbox/project-static/dist/netbox-print.css
vendored
Binary file not shown.
@ -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
Block a user