Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2021-10-06 14:04:53 -04:00
commit ca59cd1eb8
43 changed files with 343 additions and 317 deletions

View File

@ -17,7 +17,7 @@ body:
What version of NetBox are you currently running? (If you don't have access to the most 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/) 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.) before opening a bug report to see if your issue has already been addressed.)
placeholder: v3.0.5 placeholder: v3.0.6
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

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

View File

@ -5,6 +5,13 @@ NetBox includes a `housekeeping` management command that should be run nightly.
* Clearing expired authentication sessions from the database * Clearing expired authentication sessions from the database
* Deleting changelog records older than the configured [retention time](../configuration/optional-settings.md#changelog_retention) * 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.

View File

@ -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. 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 ```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. See the [housekeeping documentation](../administration/housekeeping.md) for further details.

View File

@ -111,10 +111,10 @@ sudo systemctl restart netbox netbox-rq
## Verify Housekeeping Scheduling ## 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 ```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. See the [housekeeping documentation](../administration/housekeeping.md) for further details.

View File

@ -1,5 +1,24 @@
# NetBox v3.0 # 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) ## v3.0.5 (2021-10-04)
### Enhancements ### Enhancements
@ -8,7 +27,6 @@
* [#6423](https://github.com/netbox-community/netbox/issues/6423) - Cache rendered REST API specifications * [#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 * [#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 * [#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 ### 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 * [#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 * [#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 * [#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
--- ---

View File

@ -3,10 +3,10 @@ from rest_framework import serializers
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitStatusChoices
from circuits.models import * from circuits.models import *
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer 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 import ChoiceField
from netbox.api.serializers import ( from netbox.api.serializers import (
BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
) )
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import * 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') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer() circuit = NestedCircuitSerializer()
site = NestedSiteSerializer(required=False) site = NestedSiteSerializer(required=False, allow_null=True)
provider_network = NestedProviderNetworkSerializer(required=False) provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:

View File

@ -136,14 +136,20 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
SIDE_A = CircuitTerminationSideChoices.SIDE_A SIDE_A = CircuitTerminationSideChoices.SIDE_A
SIDE_Z = CircuitTerminationSideChoices.SIDE_Z 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 = ( sites = (
Site(name='Site 1', slug='site-1'), Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'), Site(name='Site 2', slug='site-2'),
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider_networks = (
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') ProviderNetwork(provider=provider, name='Provider Network 1'),
ProviderNetwork(provider=provider, name='Provider Network 2'),
)
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = ( circuits = (
Circuit(cid='Circuit 1', provider=provider, type=circuit_type), Circuit(cid='Circuit 1', provider=provider, type=circuit_type),
@ -153,10 +159,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)
circuit_terminations = ( circuit_terminations = (
CircuitTermination(circuit=circuits[0], site=sites[0], term_side=SIDE_A), CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
CircuitTermination(circuit=circuits[0], site=sites[1], term_side=SIDE_Z), CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
CircuitTermination(circuit=circuits[1], site=sites[0], term_side=SIDE_A), CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side=SIDE_Z), CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
) )
CircuitTermination.objects.bulk_create(circuit_terminations) CircuitTermination.objects.bulk_create(circuit_terminations)
@ -164,13 +170,13 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
{ {
'circuit': circuits[2].pk, 'circuit': circuits[2].pk,
'term_side': SIDE_A, 'term_side': SIDE_A,
'site': sites[1].pk, 'site': sites[0].pk,
'port_speed': 200000, 'port_speed': 200000,
}, },
{ {
'circuit': circuits[2].pk, 'circuit': circuits[2].pk,
'term_side': SIDE_Z, 'term_side': SIDE_Z,
'site': sites[1].pk, 'provider_network': provider_networks[0].pk,
'port_speed': 200000, 'port_speed': 200000,
}, },
] ]

View File

@ -10,14 +10,14 @@ from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter, ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import * from .models import *
__all__ = ( __all__ = (
'CableFilterSet', 'CableFilterSet',
'CableTerminationFilterSet', 'CableTerminationFilterSet',
@ -1184,6 +1184,10 @@ class CableFilterSet(PrimaryModelFilterSet):
method='search', method='search',
label='Search', label='Search',
) )
termination_a_type = ContentTypeFilter()
termination_a_id = MultiValueNumberFilter()
termination_b_type = ContentTypeFilter()
termination_b_id = MultiValueNumberFilter()
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=CableTypeChoices choices=CableTypeChoices
) )
@ -1228,7 +1232,7 @@ class CableFilterSet(PrimaryModelFilterSet):
class Meta: class Meta:
model = Cable 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): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1243,73 +1247,6 @@ class CableFilterSet(PrimaryModelFilterSet):
return queryset 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): class PowerPanelFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
@ -1441,3 +1378,52 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathE
Q(comments__icontains=value) Q(comments__icontains=value)
) )
return queryset.filter(qs_filter) 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 = []

View File

@ -132,14 +132,18 @@ class RackElevationSVG:
@staticmethod @staticmethod
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation): def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
link = drawing.add( link_url = '{}?{}'.format(
drawing.a(
href='{}?{}'.format(
reverse('dcim:device_add'), reverse('dcim:device_add'),
urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_}) urlencode({
), 'site': rack.site.pk,
target='_top' 'location': rack.location.pk if rack.location else '',
'rack': rack.pk,
'face': face_id,
'position': id_
})
) )
link = drawing.add(
drawing.a(href=link_url, target='_top')
) )
if reservation: if reservation:
link.set_desc('{}{} · {}'.format( link.set_desc('{}{} · {}'.format(

View File

@ -2851,6 +2851,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
Interface.objects.bulk_create(interfaces) 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 # 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[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() 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[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[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=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): def test_label(self):
params = {'label': ['Cable 1', 'Cable 2']} params = {'label': ['Cable 1', 'Cable 2']}
@ -2877,7 +2881,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_status(self): def test_status(self):
params = {'status': [CableStatusChoices.STATUS_CONNECTED]} 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]} params = {'status': [CableStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@ -2888,30 +2892,44 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_device(self): def test_device(self):
devices = Device.objects.all()[:2] devices = Device.objects.all()[:2]
params = {'device_id': [devices[0].pk, devices[1].pk]} 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]} 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): def test_rack(self):
racks = Rack.objects.all()[:2] racks = Rack.objects.all()[:2]
params = {'rack_id': [racks[0].pk, racks[1].pk]} 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]} 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): def test_site(self):
site = Site.objects.all()[:2] site = Site.objects.all()[:2]
params = {'site_id': [site[0].pk, site[1].pk]} 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]} 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): def test_tenant(self):
tenant = Tenant.objects.all()[:2] tenant = Tenant.objects.all()[:2]
params = {'tenant_id': [tenant[0].pk, tenant[1].pk]} 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]} 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): class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):

View File

@ -1229,6 +1229,7 @@ class PlatformView(generic.ObjectView):
return { return {
'devices_table': devices_table, 'devices_table': devices_table,
'virtualmachine_count': VirtualMachine.objects.filter(platform=instance).count()
} }

View File

@ -1,3 +1,4 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from drf_yasg.utils import swagger_serializer_method from drf_yasg.utils import swagger_serializer_method
@ -30,6 +31,7 @@ __all__ = (
'ExportTemplateSerializer', 'ExportTemplateSerializer',
'ImageAttachmentSerializer', 'ImageAttachmentSerializer',
'JobResultSerializer', 'JobResultSerializer',
'JournalEntrySerializer',
'ObjectChangeSerializer', 'ObjectChangeSerializer',
'ReportDetailSerializer', 'ReportDetailSerializer',
'ReportSerializer', 'ReportSerializer',
@ -192,6 +194,12 @@ class JournalEntrySerializer(ValidatedModelSerializer):
queryset=ContentType.objects.all() queryset=ContentType.objects.all()
) )
assigned_object = serializers.SerializerMethodField(read_only=True) 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( kind = ChoiceField(
choices=JournalEntryKindChoices, choices=JournalEntryKindChoices,
required=False required=False

View File

@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup # Environment setup
# #
VERSION = '3.0.6-dev' VERSION = '3.0.7-dev'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -73,16 +73,6 @@
color: color-contrast($value); 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. // 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 { table {
td {
a { a {
text-decoration: none; text-decoration: none;
&:hover { &:hover {
text-decoration: underline; 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, td,
th { th {
font-size: $font-size-sm; 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 { &.object-list {
th { th {
font-size: $font-size-xs; font-size: $font-size-xs;

View File

@ -70,6 +70,7 @@ $spacing-s: $input-padding-x;
span.arrow-down, span.arrow-down,
span.arrow-up { span.arrow-up {
border-color: currentColor; border-color: currentColor;
color: $text-muted;
} }
} }
// Don't show the depth indicator outside of the menu. // Don't show the depth indicator outside of the menu.

View File

@ -7,6 +7,7 @@ $input-border-color: $gray-200;
$theme-colors: map-merge( $theme-colors: map-merge(
$theme-colors, $theme-colors,
( (
'primary': #337ab7,
'red': $red-500, 'red': $red-500,
'yellow': $yellow-500, 'yellow': $yellow-500,
'green': $green-500, 'green': $green-500,

View File

@ -23,7 +23,7 @@
--nbx-color-mode-toggle-color: #{$primary}; --nbx-color-mode-toggle-color: #{$primary};
--nbx-sidenav-link-color: #{$gray-800}; --nbx-sidenav-link-color: #{$gray-800};
--nbx-sidenav-pin-color: #{$orange}; --nbx-sidenav-pin-color: #{$orange};
--nbx-sidenav-parent-color: #{$gray-900}; --nbx-sidenav-parent-color: #{$gray-800};
--nbx-sidenav-group-color: #{$gray-800}; --nbx-sidenav-group-color: #{$gray-800};
&[data-netbox-color-mode='dark'] { &[data-netbox-color-mode='dark'] {
@ -49,7 +49,7 @@
--nbx-color-mode-toggle-color: #{$yellow-300}; --nbx-color-mode-toggle-color: #{$yellow-300};
--nbx-sidenav-link-color: #{$gray-200}; --nbx-sidenav-link-color: #{$gray-200};
--nbx-sidenav-pin-color: #{$yellow}; --nbx-sidenav-pin-color: #{$yellow};
--nbx-sidenav-parent-color: #{$gray-100}; --nbx-sidenav-parent-color: #{$gray-200};
--nbx-sidenav-group-color: #{$gray-600}; --nbx-sidenav-group-color: #{$gray-600};
} }
} }

View File

@ -5,7 +5,7 @@
{% block title %}{{ obj.circuit.provider }} {{ obj.circuit }} - Side {{ form.term_side.value }}{% endblock %} {% block title %}{{ obj.circuit.provider }} {{ obj.circuit }} - Side {{ form.term_side.value }}{% endblock %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Circuit Termination</h5> <h5 class="offset-sm-3">Circuit Termination</h5>
</div> </div>
@ -53,9 +53,8 @@
</div> </div>
{% endwith %} {% endwith %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Termination Details</h5> <h5 class="offset-sm-3">Termination Details</h5>
</div> </div>

View File

@ -17,9 +17,7 @@
<div class="row my-3"> <div class="row my-3">
<div class="col col-md-5"> <div class="col col-md-5">
<div class="card h-100"> <div class="card h-100">
<h5 class="card-header"> <h5 class="card-header offset-sm-3">A Side</h5>
A Side
</h5>
<div class="card-body"> <div class="card-body">
{% if termination_a.device %} {% if termination_a.device %}
{# Device component #} {# Device component #}
@ -100,9 +98,7 @@
</div> </div>
<div class="col col-md-5"> <div class="col col-md-5">
<div class="card h-100"> <div class="card h-100">
<h5 class="card-header"> <h5 class="card-header offset-sm-3">B Side</h5>
B Side
</h5>
<div class="card-body"> <div class="card-body">
{% if tabs %} {% if tabs %}
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
@ -154,7 +150,7 @@
<div class="row my-3 justify-content-center"> <div class="row my-3 justify-content-center">
<div class="col col-md-8"> <div class="col col-md-8">
<div class="card"> <div class="card">
<h5 class="card-header">Cable</h5> <h5 class="card-header offset-sm-3">Cable</h5>
<div class="card-body"> <div class="card-body">
{% include 'dcim/inc/cable_form.html' %} {% include 'dcim/inc/cable_form.html' %}
</div> </div>

View File

@ -4,7 +4,7 @@
{% block form %} {% block form %}
{% render_errors form %} {% render_errors form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Device</h5> <h5 class="offset-sm-3">Device</h5>
</div> </div>
@ -12,9 +12,8 @@
{% render_field form.device_role %} {% render_field form.device_role %}
{% render_field form.tags %} {% render_field form.tags %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Hardware</h5> <h5 class="offset-sm-3">Hardware</h5>
</div> </div>
@ -23,9 +22,8 @@
{% render_field form.serial %} {% render_field form.serial %}
{% render_field form.asset_tag %} {% render_field form.asset_tag %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Location</h5> <h5 class="offset-sm-3">Location</h5>
</div> </div>
@ -58,9 +56,8 @@
{% render_field form.position %} {% render_field form.position %}
{% endif %} {% endif %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Management</h5> <h5 class="offset-sm-3">Management</h5>
</div> </div>
@ -71,44 +68,40 @@
{% render_field form.primary_ip6 %} {% render_field form.primary_ip6 %}
{% endif %} {% endif %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Virtualization</h5> <h5 class="offset-sm-3">Virtualization</h5>
</div> </div>
{% render_field form.cluster_group %} {% render_field form.cluster_group %}
{% render_field form.cluster %} {% render_field form.cluster %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Tenancy</h5> <h5 class="offset-sm-3">Tenancy</h5>
</div> </div>
{% render_field form.tenant_group %} {% render_field form.tenant_group %}
{% render_field form.tenant %} {% render_field form.tenant %}
</div> </div>
<hr />
{% if form.custom_fields %} {% if form.custom_fields %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>
{% render_custom_fields form %} {% render_custom_fields form %}
</div> </div>
<hr />
{% endif %} {% endif %}
<div class="field-group my-4"> <div class="field-group my-5">
<h5 class="text-center">Local Config Context Data</h5> <h5 class="text-center">Local Config Context Data</h5>
{% render_field form.local_context_data %} {% render_field form.local_context_data %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group mb-5">
{% render_field form.comments label='Comments' %} <h5 class="text-center">Comments</h5>
{% render_field form.comments %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -16,7 +16,6 @@
</div> </div>
{% render_field form.tags %} {% render_field form.tags %}
{% if form.custom_fields %} {% if form.custom_fields %}
<hr />
<div class="field-group"> <div class="field-group">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>

View File

@ -2,7 +2,7 @@
{% load form_helpers %} {% load form_helpers %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Interface</h5> <h5 class="offset-sm-3">Interface</h5>
</div> </div>
@ -27,9 +27,8 @@
{% render_field form.mgmt_only %} {% render_field form.mgmt_only %}
{% render_field form.mark_connected %} {% render_field form.mark_connected %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">802.1Q Switching</h5> <h5 class="offset-sm-3">802.1Q Switching</h5>
</div> </div>
@ -40,8 +39,7 @@
</div> </div>
{% if form.custom_fields %} {% if form.custom_fields %}
<hr /> <div class="field-group my-5">
<div class="field-group my-4">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>

View File

@ -46,6 +46,12 @@
<a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ devices_table.rows|length }}</a> <a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
</td> </td>
</tr> </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> </table>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load form_helpers %} {% load form_helpers %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Rack</h5> <h5 class="offset-sm-3">Rack</h5>
</div> </div>
@ -15,9 +15,8 @@
{% render_field form.role %} {% render_field form.role %}
{% render_field form.tags %} {% render_field form.tags %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Inventory Control</h5> <h5 class="offset-sm-3">Inventory Control</h5>
</div> </div>
@ -25,18 +24,16 @@
{% render_field form.serial %} {% render_field form.serial %}
{% render_field form.asset_tag %} {% render_field form.asset_tag %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Tenancy</h5> <h5 class="offset-sm-3">Tenancy</h5>
</div> </div>
{% render_field form.tenant_group %} {% render_field form.tenant_group %}
{% render_field form.tenant %} {% render_field form.tenant %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Dimensions</h5> <h5 class="offset-sm-3">Dimensions</h5>
</div> </div>
@ -45,34 +42,33 @@
{% render_field form.u_height %} {% render_field form.u_height %}
<div class="row mb-3"> <div class="row mb-3">
<label class="col col-md-3 col-form-label text-lg-end">Outer Dimensions</label> <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 }} {{ form.outer_width }}
<div class="form-text">Width</div> <div class="form-text">Width</div>
</div> </div>
<div class="col col-md-3"> <div class="col col-md-3 mb-1">
{{ form.outer_depth }} {{ form.outer_depth }}
<div class="form-text">Depth</div> <div class="form-text">Depth</div>
</div> </div>
<div class="col col-md-3"> <div class="col col-md-3 mb-1">
{{ form.outer_unit }} {{ form.outer_unit }}
<div class="form-text">Unit</div> <div class="form-text">Unit</div>
</div> </div>
</div> </div>
{% render_field form.desc_units %} {% render_field form.desc_units %}
</div> </div>
<hr />
{% if form.custom_fields %} {% if form.custom_fields %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>
{% render_custom_fields form %} {% render_custom_fields form %}
</div> </div>
<hr />
{% endif %} {% endif %}
<div class="field-group my-4"> <div class="field-group my-5">
{% render_field form.comments label='Comments' %} <h5 class="text-center">Comments</h5>
{% render_field form.comments %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{% load form_helpers %} {% load form_helpers %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Virtual Chassis</h5> <h5 class="offset-sm-3">Virtual Chassis</h5>
</div> </div>
@ -10,9 +10,8 @@
{% render_field form.domain %} {% render_field form.domain %}
{% render_field form.tags %} {% render_field form.tags %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Member Devices</h5> <h5 class="offset-sm-3">Member Devices</h5>
</div> </div>
@ -25,8 +24,7 @@
</div> </div>
{% if form.custom_fields %} {% if form.custom_fields %}
<hr /> <div class="field-group my-5">
<div class="field-group my-4">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>

View File

@ -11,7 +11,7 @@
{% csrf_token %} {% csrf_token %}
{{ pk_form.pk }} {{ pk_form.pk }}
{{ formset.management_form }} {{ formset.management_form }}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Virtual Chassis</h5> <h5 class="offset-sm-3">Virtual Chassis</h5>
</div> </div>
@ -20,16 +20,14 @@
{% render_field vc_form.master %} {% render_field vc_form.master %}
{% render_field vc_form.tags %} {% render_field vc_form.tags %}
</div> </div>
<hr />
{% if vc_form.custom_fields %} {% if vc_form.custom_fields %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>
{% render_custom_fields vc_form %} {% render_custom_fields vc_form %}
</div> </div>
<hr />
{% endif %} {% endif %}
<div class="field-group mb-5"> <div class="field-group mb-5">

View File

@ -8,6 +8,12 @@
{% block header %} {% block header %}
{# Breadcrumbs #} {# Breadcrumbs #}
<nav class="breadcrumb-container px-3" aria-label="breadcrumb"> <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"> <ol class="breadcrumb">
{% block breadcrumbs %} {% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li> <li class="breadcrumb-item"><a href="{% url object|viewname:'list' %}">{{ object|meta:'verbose_name_plural'|bettertitle }}</a></li>

View File

@ -6,18 +6,6 @@
{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %} {% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}
{% endblock title %} {% 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 %} {% block tabs %}
<ul class="nav nav-tabs px-3"> <ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
@ -31,6 +19,16 @@
{% block content-wrapper %} {% block content-wrapper %}
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab"> <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"> <form action="" method="post" enctype="multipart/form-data" class="form-object-edit">
{% csrf_token %} {% csrf_token %}
{% for field in form.hidden_fields %} {% for field in form.hidden_fields %}
@ -42,7 +40,7 @@
{# Render grouped fields according to Form #} {# Render grouped fields according to Form #}
{% for group, fields in form.Meta.fieldsets %} {% for group, fields in form.Meta.fieldsets %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">{{ group }}</h5> <h5 class="offset-sm-3">{{ group }}</h5>
</div> </div>
@ -50,14 +48,10 @@
{% render_field form|getfield:name %} {% render_field form|getfield:name %}
{% endfor %} {% endfor %}
</div> </div>
{% if not forloop.last %}
<hr />
{% endif %}
{% endfor %} {% endfor %}
{% if form.custom_fields %} {% if form.custom_fields %}
<hr /> <div class="field-group my-5">
<div class="field-group my-4">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>
@ -66,15 +60,15 @@
{% endif %} {% endif %}
{% if form.comments %} {% if form.comments %}
<hr /> <div class="field-group my-5">
<div class="field-group my-4"> <h5 class="text-center">Comments</h5>
{% render_field form.comments label='Comments' %} {% render_field form.comments %}
</div> </div>
{% endif %} {% endif %}
{% else %} {% else %}
{# Render all fields in a single group #} {# 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 %} {% block form_fields %}{% render_form form %}{% endblock %}
</div> </div>
{% endif %} {% endif %}

View File

@ -4,6 +4,8 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load static %} {% load static %}
{% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
{% block controls %} {% block controls %}
<div class="controls"> <div class="controls">
<div class="control-group"> <div class="control-group">
@ -26,7 +28,7 @@
{% block tab_items %} {% block tab_items %}
<li class="nav-item" role="presentation"> <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"> <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 %} {% badge table.page.paginator.count %}
</button> </button>
</li> </li>

View File

@ -30,7 +30,7 @@
{% for section, items, icon in stats %} {% for section, items, icon in stats %}
<div class="col col-sm-12 col-lg-6 col-xl-4 my-2 masonry-item"> <div class="col col-sm-12 col-lg-6 col-xl-4 my-2 masonry-item">
<div class="card"> <div class="card">
<h6 class="card-header text-primary text-center"> <h6 class="card-header text-center">
<i class="mdi mdi-{{ icon }}"></i> <i class="mdi mdi-{{ icon }}"></i>
<span class="ms-1">{{ section }}</span> <span class="ms-1">{{ section }}</span>
</h6> </h6>
@ -67,7 +67,7 @@
<div class="row my-4 flex-grow-1 changelog-container"> <div class="row my-4 flex-grow-1 changelog-container">
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<h6 class="card-header text-primary text-center"> <h6 class="card-header text-center">
<i class="mdi mdi-clipboard-clock"></i> <i class="mdi mdi-clipboard-clock"></i>
<span class="ms-1">Change Log</span> <span class="ms-1">Change Log</span>
</h6> </h6>

View File

@ -4,8 +4,8 @@
<input <input
type="text" type="text"
class="form-control object-filter" class="form-control object-filter"
placeholder="Filter" placeholder="Quick find"
title="Filter text (regular expressions supported)" title="Find in the results below (regular expressions supported)"
/> />
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@
{% endblock %} {% endblock %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">IP Addresses</h5> <h5 class="offset-sm-3">IP Addresses</h5>
</div> </div>
@ -20,9 +20,8 @@
{% render_field model_form.description %} {% render_field model_form.description %}
{% render_field model_form.tags %} {% render_field model_form.tags %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Tenancy</h5> <h5 class="offset-sm-3">Tenancy</h5>
</div> </div>
@ -30,8 +29,7 @@
{% render_field model_form.tenant %} {% render_field model_form.tenant %}
</div> </div>
{% if model_form.custom_fields %} {% if model_form.custom_fields %}
<hr /> <div class="field-group my-5">
<div class="field-group my-4">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>

View File

@ -8,7 +8,7 @@
{% endblock tabs %} {% endblock tabs %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">IP Address</h5> <h5 class="offset-sm-3">IP Address</h5>
</div> </div>
@ -20,18 +20,16 @@
{% render_field form.description %} {% render_field form.description %}
{% render_field form.tags %} {% render_field form.tags %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Tenancy</h5> <h5 class="offset-sm-3">Tenancy</h5>
</div> </div>
{% render_field form.tenant_group %} {% render_field form.tenant_group %}
{% render_field form.tenant %} {% render_field form.tenant %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Interface Assignment</h5> <h5 class="offset-sm-3">Interface Assignment</h5>
</div> </div>
@ -81,9 +79,8 @@
</div> </div>
{% endwith %} {% endwith %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">NAT IP (Inside)</h5> <h5 class="offset-sm-3">NAT IP (Inside)</h5>
</div> </div>
@ -152,8 +149,7 @@
</div> </div>
{% if form.custom_fields %} {% if form.custom_fields %}
<hr /> <div class="field-group my-5">
<div class="field-group my-4">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>

View File

@ -2,7 +2,7 @@
{% load form_helpers %} {% load form_helpers %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Service</h5> <h5 class="offset-sm-3">Service</h5>
</div> </div>
@ -43,7 +43,6 @@
</div> </div>
{% if form.custom_fields %} {% if form.custom_fields %}
<hr />
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>

View File

@ -4,7 +4,7 @@
{% load helpers %} {% load helpers %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">VLAN</h5> <h5 class="offset-sm-3">VLAN</h5>
</div> </div>
@ -15,18 +15,16 @@
{% render_field form.description %} {% render_field form.description %}
{% render_field form.tags %} {% render_field form.tags %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Tenancy</h5> <h5 class="offset-sm-3">Tenancy</h5>
</div> </div>
{% render_field form.tenant_group %} {% render_field form.tenant_group %}
{% render_field form.tenant %} {% render_field form.tenant %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Assignment</h5> <h5 class="offset-sm-3">Assignment</h5>
</div> </div>
@ -58,8 +56,7 @@
</div> </div>
{% if form.custom_fields %} {% if form.custom_fields %}
<hr /> <div class="field-group my-5">
<div class="field-group my-4">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>

View File

@ -37,7 +37,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Sites</th> <th scope="row">Tenants</th>
<td> <td>
<a href="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}">{{ tenants_table.rows|length }}</a> <a href="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}">{{ tenants_table.rows|length }}</a>
</td> </td>

View File

@ -2,7 +2,7 @@
{% load form_helpers %} {% load form_helpers %}
{% block form %} {% block form %}
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Interface</h5> <h5 class="offset-sm-3">Interface</h5>
</div> </div>
@ -22,9 +22,8 @@
{% render_field form.description %} {% render_field form.description %}
{% render_field form.tags %} {% render_field form.tags %}
</div> </div>
<hr />
<div class="field-group my-4"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">802.1Q Switching</h5> <h5 class="offset-sm-3">802.1Q Switching</h5>
</div> </div>
@ -35,8 +34,7 @@
</div> </div>
{% if form.custom_fields %} {% if form.custom_fields %}
<hr /> <div class="field-group my-5">
<div class="field-group my-4">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">Custom Fields</h5>
</div> </div>

View File

@ -57,14 +57,14 @@ class BaseTable(tables.Table):
if user is not None and not isinstance(user, AnonymousUser): if user is not None and not isinstance(user, AnonymousUser):
selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns") selected_columns = user.config.get(f"tables.{self.__class__.__name__}.columns")
if selected_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(): for name, column in self.columns.items():
if name in selected_columns: if name in ['pk', 'actions', *selected_columns]:
self.columns.show(name) self.columns.show(name)
else: else:
self.columns.hide(name) self.columns.hide(name)
# Rearrange the sequence to list selected columns first, followed by all remaining columns # Rearrange the sequence to list selected columns first, followed by all remaining columns
# TODO: There's probably a more clever way to accomplish this # TODO: There's probably a more clever way to accomplish this
self.sequence = [ self.sequence = [
@ -72,12 +72,14 @@ class BaseTable(tables.Table):
*[c for c in self.columns.names() if c not in selected_columns] *[c for c in self.columns.names() if c not in selected_columns]
] ]
# Always include PK and actions column, if defined on the table # PK column should always come first
if pk: if 'pk' in self.sequence:
self.base_columns['pk'] = pk self.sequence.remove('pk')
self.sequence.insert(0, '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') self.sequence.append('actions')
# Dynamically update the table's QuerySet to ensure related fields are pre-fetched # 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. prefixes/IP addresses/etc., where some table rows may represent available address space.
""" """
if not hasattr(self, '_objects_count'): 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 return self._objects_count

View File

@ -1,5 +1,5 @@
Django==3.2.7 Django==3.2.8
django-cors-headers==3.9.0 django-cors-headers==3.10.0
django-debug-toolbar==3.2.2 django-debug-toolbar==3.2.2
django-filter==21.1 django-filter==21.1
django-graphiql-debug-toolbar==0.2.0 django-graphiql-debug-toolbar==0.2.0
@ -8,14 +8,14 @@ django-pglocks==1.0.4
django-prometheus==2.1.0 django-prometheus==2.1.0
django-redis==5.0.0 django-redis==5.0.0
django-rq==2.4.1 django-rq==2.4.1
django-tables2==2.4.0 django-tables2==2.4.1
django-taggit==1.5.1 django-taggit==1.5.1
django-timezone-field==4.2.1 django-timezone-field==4.2.1
djangorestframework==3.12.4 djangorestframework==3.12.4
drf-yasg[validation]==1.20.0 drf-yasg[validation]==1.20.0
graphene_django==2.15.0 graphene_django==2.15.0
gunicorn==20.1.0 gunicorn==20.1.0
Jinja2==3.0.1 Jinja2==3.0.2
Markdown==3.3.4 Markdown==3.3.4
markdown-include==0.6.0 markdown-include==0.6.0
mkdocs-material==7.3.1 mkdocs-material==7.3.1