Merge branch 'feature' into 17476-upgrade-django51

This commit is contained in:
Arthur Hanson 2024-10-02 12:47:01 -07:00
commit 58d3838c4b
29 changed files with 171 additions and 97 deletions

View File

@ -96,14 +96,6 @@ The maximum size (in bytes) of an incoming HTTP request (i.e. `GET` or `POST` da
--- ---
## DJANGO_ADMIN_ENABLED
Default: False
Setting this to True installs the `django.contrib.admin` app and enables the [Django admin UI](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/). This may be necessary to support older plugins which do not integrate with the native NetBox interface.
---
## ENFORCE_GLOBAL_UNIQUE ## ENFORCE_GLOBAL_UNIQUE
!!! tip "Dynamic Configuration Parameter" !!! tip "Dynamic Configuration Parameter"

View File

@ -44,3 +44,7 @@ The serial number assigned by the manufacturer.
### Asset Tag ### Asset Tag
A unique, locally-administered label used to identify hardware resources. A unique, locally-administered label used to identify hardware resources.
### Status
The inventory item's operational status.

View File

@ -29,6 +29,10 @@ An alternative physical label identifying the power outlet.
The type of power outlet. The type of power outlet.
### Color
The power outlet's color (optional).
### Power Port ### Power Port
When modeling a device which redistributes power from an upstream supply, such as a power distribution unit (PDU), each power outlet should be mapped to the respective [power port](./powerport.md) on the device which supplies power. For example, a 24-outlet PDU may two power ports, each distributing power to 12 of its outlets. When modeling a device which redistributes power from an upstream supply, such as a power distribution unit (PDU), each power outlet should be mapped to the respective [power port](./powerport.md) on the device which supplies power. For example, a 24-outlet PDU may two power ports, each distributing power to 12 of its outlets.

View File

@ -155,7 +155,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'power_port',
'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'feed_leg', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied', 'created', 'last_updated', '_occupied',
@ -345,11 +345,12 @@ class InventoryItemSerializer(NetBoxModelSerializer):
) )
component = serializers.SerializerMethodField(read_only=True, allow_null=True) component = serializers.SerializerMethodField(read_only=True, allow_null=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
status = ChoiceField(choices=InventoryItemStatusChoices, required=False)
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'id', 'url', 'display_url', 'display', 'device', 'parent', 'name', 'label', 'status', 'role', 'manufacturer',
'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', 'component_type', 'component_id',
'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth', 'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
] ]

View File

@ -1648,3 +1648,27 @@ class VirtualDeviceContextStatusChoices(ChoiceSet):
(STATUS_PLANNED, _('Planned'), 'cyan'), (STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_OFFLINE, _('Offline'), 'red'), (STATUS_OFFLINE, _('Offline'), 'red'),
] ]
#
# InventoryItem
#
class InventoryItemStatusChoices(ChoiceSet):
key = 'InventoryItem.status'
STATUS_OFFLINE = 'offline'
STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
STATUS_STAGED = 'staged'
STATUS_FAILED = 'failed'
STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = [
(STATUS_OFFLINE, _('Offline'), 'gray'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_STAGED, _('Staged'), 'blue'),
(STATUS_FAILED, _('Failed'), 'red'),
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
]

View File

@ -1594,7 +1594,7 @@ class PowerOutletFilterSet(
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ( fields = (
'id', 'name', 'label', 'feed_leg', 'description', 'mark_connected', 'cable_end', 'id', 'name', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
) )
@ -1860,10 +1860,14 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
serial = MultiValueCharFilter( serial = MultiValueCharFilter(
lookup_expr='iexact' lookup_expr='iexact'
) )
status = django_filters.MultipleChoiceFilter(
choices=InventoryItemStatusChoices,
null_value=None
)
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'description', 'discovered') fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'status', 'description', 'discovered')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -69,7 +69,7 @@ class PowerPortBulkCreateForm(
class PowerOutletBulkCreateForm( class PowerOutletBulkCreateForm(
form_from_model(PowerOutlet, ['type', 'feed_leg', 'mark_connected']), form_from_model(PowerOutlet, ['type', 'color', 'feed_leg', 'mark_connected']),
DeviceBulkAddComponentForm DeviceBulkAddComponentForm
): ):
model = PowerOutlet model = PowerOutlet

View File

@ -1361,7 +1361,7 @@ class PowerPortBulkEditForm(
class PowerOutletBulkEditForm( class PowerOutletBulkEditForm(
ComponentBulkEditForm, ComponentBulkEditForm,
form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']) form_from_model(PowerOutlet, ['label', 'type', 'color', 'feed_leg', 'power_port', 'mark_connected', 'description'])
): ):
mark_connected = forms.NullBooleanField( mark_connected = forms.NullBooleanField(
label=_('Mark connected'), label=_('Mark connected'),
@ -1371,7 +1371,7 @@ class PowerOutletBulkEditForm(
model = PowerOutlet model = PowerOutlet
fieldsets = ( fieldsets = (
FieldSet('module', 'type', 'label', 'description', 'mark_connected'), FieldSet('module', 'type', 'label', 'description', 'mark_connected', 'color'),
FieldSet('feed_leg', 'power_port', name=_('Power')), FieldSet('feed_leg', 'power_port', name=_('Power')),
) )
nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description') nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description')
@ -1661,10 +1661,16 @@ class InventoryItemBulkEditForm(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(InventoryItemStatusChoices),
required=False,
initial=''
)
model = InventoryItem model = InventoryItem
fieldsets = ( fieldsets = (
FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'description'), FieldSet('device', 'label', 'role', 'manufacturer', 'part_id', 'status', 'description'),
) )
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')

View File

@ -798,7 +798,7 @@ class PowerOutletImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags') fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -1103,11 +1103,16 @@ class InventoryItemImportForm(NetBoxModelImportForm):
required=False, required=False,
help_text=_('Component Name') help_text=_('Component Name')
) )
status = CSVChoiceField(
label=_('Status'),
choices=InventoryItemStatusChoices,
help_text=_('Operational status')
)
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ( fields = (
'device', 'name', 'label', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered', 'device', 'name', 'label', 'status', 'role', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags', 'component_type', 'component_name', 'description', 'tags', 'component_type', 'component_name',
) )

View File

@ -35,7 +35,6 @@ __all__ = (
'LocationFilterForm', 'LocationFilterForm',
'ManufacturerFilterForm', 'ManufacturerFilterForm',
'ModuleFilterForm', 'ModuleFilterForm',
'ModuleFilterForm',
'ModuleBayFilterForm', 'ModuleBayFilterForm',
'ModuleTypeFilterForm', 'ModuleTypeFilterForm',
'PlatformFilterForm', 'PlatformFilterForm',
@ -1304,7 +1303,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet model = PowerOutlet
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet('name', 'label', 'type', name=_('Attributes')), FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet( FieldSet(
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@ -1318,6 +1317,10 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)
color = ColorField(
label=_('Color'),
required=False
)
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
@ -1553,6 +1556,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
status = forms.MultipleChoiceField(
label=_('Status'),
choices=InventoryItemStatusChoices,
required=False
)
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -1285,7 +1285,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
fieldsets = ( fieldsets = (
FieldSet( FieldSet(
'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', 'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected', 'description',
'tags', 'tags',
), ),
) )
@ -1293,7 +1293,7 @@ class PowerOutletForm(ModularDeviceComponentForm):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description', 'device', 'module', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'mark_connected', 'description',
'tags', 'tags',
] ]
@ -1576,7 +1576,7 @@ class InventoryItemForm(DeviceComponentForm):
) )
fieldsets = ( fieldsets = (
FieldSet('device', 'parent', 'name', 'label', 'role', 'description', 'tags', name=_('Inventory Item')), FieldSet('device', 'parent', 'name', 'label', 'status', 'role', 'description', 'tags', name=_('Inventory Item')),
FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')), FieldSet('manufacturer', 'part_id', 'serial', 'asset_tag', name=_('Hardware')),
FieldSet( FieldSet(
TabbedGroups( TabbedGroups(
@ -1596,7 +1596,7 @@ class InventoryItemForm(DeviceComponentForm):
model = InventoryItem model = InventoryItem
fields = [ fields = [
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
'description', 'tags', 'status', 'description', 'tags',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -568,6 +568,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin):
) )
class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None
color: str
@strawberry_django.type( @strawberry_django.type(

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2024-09-26 20:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0190_nested_modules'),
]
operations = [
migrations.AddField(
model_name='inventoryitem',
name='status',
field=models.CharField(default='active', max_length=50),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.0.9 on 2024-09-26 19:31
import utilities.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0190_nested_modules'),
]
operations = [
migrations.AddField(
model_name='poweroutlet',
name='color',
field=utilities.fields.ColorField(blank=True, max_length=6),
),
]

View File

@ -481,6 +481,10 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
blank=True, blank=True,
help_text=_('Phase (for three-phase feeds)') help_text=_('Phase (for three-phase feeds)')
) )
color = ColorField(
verbose_name=_('color'),
blank=True
)
clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg') clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
@ -1244,6 +1248,12 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
ct_field='component_type', ct_field='component_type',
fk_field='component_id' fk_field='component_id'
) )
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=InventoryItemStatusChoices,
default=InventoryItemStatusChoices.STATUS_ACTIVE
)
role = models.ForeignKey( role = models.ForeignKey(
to='dcim.InventoryItemRole', to='dcim.InventoryItemRole',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -1285,7 +1295,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
objects = TreeManager() objects = TreeManager()
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',) clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
class Meta: class Meta:
ordering = ('device__id', 'parent__id', '_name') ordering = ('device__id', 'parent__id', '_name')
@ -1334,3 +1344,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
raise ValidationError({ raise ValidationError({
"device": _("Cannot assign inventory item to component on another device") "device": _("Cannot assign inventory item to component on another device")
}) })
def get_status_color(self):
return InventoryItemStatusChoices.colors.get(self.status)

View File

@ -512,6 +512,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
verbose_name=_('Power Port'), verbose_name=_('Power Port'),
linkify=True linkify=True
) )
color = columns.ColorColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:poweroutlet_list' url_name='dcim:poweroutlet_list'
) )
@ -520,10 +521,10 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
model = models.PowerOutlet model = models.PowerOutlet
fields = ( fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port', 'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'description', 'power_port',
'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items', 'color', 'feed_leg', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'inventory_items',
'tags', 'created', 'last_updated', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'device', 'label', 'type', 'power_port', 'feed_leg', 'description') default_columns = ('pk', 'name', 'device', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description')
class DevicePowerOutletTable(PowerOutletTable): class DevicePowerOutletTable(PowerOutletTable):
@ -540,11 +541,11 @@ class DevicePowerOutletTable(PowerOutletTable):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.PowerOutlet model = models.PowerOutlet
fields = ( fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description', 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'power_port', 'feed_leg',
'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection', 'pk', 'name', 'label', 'type', 'color', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
) )
@ -946,6 +947,9 @@ class InventoryItemTable(DeviceComponentTable):
verbose_name=_('Discovered'), verbose_name=_('Discovered'),
false_mark=None false_mark=None
) )
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
parent = tables.Column( parent = tables.Column(
linkify=True, linkify=True,
verbose_name=_('Parent'), verbose_name=_('Parent'),
@ -958,11 +962,11 @@ class InventoryItemTable(DeviceComponentTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = models.InventoryItem model = models.InventoryItem
fields = ( fields = (
'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'pk', 'id', 'name', 'device', 'parent', 'component', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated', 'asset_tag', 'description', 'discovered', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'device', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'pk', 'name', 'device', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
) )
@ -978,11 +982,11 @@ class DeviceInventoryItemTable(InventoryItemTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = models.InventoryItem model = models.InventoryItem
fields = ( fields = (
'pk', 'id', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'pk', 'id', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
'description', 'discovered', 'tags', 'actions', 'description', 'discovered', 'tags', 'actions',
) )
default_columns = ( default_columns = (
'pk', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component', 'pk', 'name', 'label', 'status', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'component',
) )

View File

@ -3421,9 +3421,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
PowerPort.objects.bulk_create(power_ports) PowerPort.objects.bulk_create(power_ports)
power_outlets = ( power_outlets = (
PowerOutlet(device=devices[0], module=modules[0], name='Power Outlet 1', label='A', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'), PowerOutlet(device=devices[0], module=modules[0], name='Power Outlet 1', label='A', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First', color='ff0000'),
PowerOutlet(device=devices[1], module=modules[1], name='Power Outlet 2', label='B', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'), PowerOutlet(device=devices[1], module=modules[1], name='Power Outlet 2', label='B', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second', color='00ff00'),
PowerOutlet(device=devices[2], module=modules[2], name='Power Outlet 3', label='C', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'), PowerOutlet(device=devices[2], module=modules[2], name='Power Outlet 3', label='C', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third', color='0000ff'),
) )
PowerOutlet.objects.bulk_create(power_outlets) PowerOutlet.objects.bulk_create(power_outlets)
@ -3444,6 +3444,10 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
params = {'description': ['First', 'Second']} params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_color(self):
params = {'color': ['ff0000', '00ff00']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_feed_leg(self): def test_feed_leg(self):
params = {'feed_leg': [PowerOutletFeedLegChoices.FEED_LEG_A, PowerOutletFeedLegChoices.FEED_LEG_B]} params = {'feed_leg': [PowerOutletFeedLegChoices.FEED_LEG_A, PowerOutletFeedLegChoices.FEED_LEG_B]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -4751,9 +4755,9 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
inventory_items = ( inventory_items = (
InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, description='First', component=components[0]), InventoryItem(device=devices[0], role=roles[0], manufacturer=manufacturers[0], name='Inventory Item 1', label='A', part_id='1001', serial='ABC', asset_tag='1001', discovered=True, status=ModuleStatusChoices.STATUS_ACTIVE, description='First', component=components[0]),
InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, description='Second', component=components[1]), InventoryItem(device=devices[1], role=roles[1], manufacturer=manufacturers[1], name='Inventory Item 2', label='B', part_id='1002', serial='DEF', asset_tag='1002', discovered=True, status=ModuleStatusChoices.STATUS_PLANNED, description='Second', component=components[1]),
InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, description='Third', component=components[2]), InventoryItem(device=devices[2], role=roles[2], manufacturer=manufacturers[2], name='Inventory Item 3', label='C', part_id='1003', serial='GHI', asset_tag='1003', discovered=False, status=ModuleStatusChoices.STATUS_FAILED, description='Third', component=components[2]),
) )
for i in inventory_items: for i in inventory_items:
i.save() i.save()
@ -4881,6 +4885,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'component_type': 'dcim.interface'} params = {'component_type': 'dcim.interface'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_status(self):
params = {'status': [InventoryItemStatusChoices.STATUS_PLANNED, InventoryItemStatusChoices.STATUS_FAILED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests): class InventoryItemRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = InventoryItemRole.objects.all() queryset = InventoryItemRole.objects.all()

View File

@ -2903,6 +2903,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
'part_id': '123456', 'part_id': '123456',
'serial': '123ABC', 'serial': '123ABC',
'asset_tag': 'ABC123', 'asset_tag': 'ABC123',
'status': InventoryItemStatusChoices.STATUS_ACTIVE,
'description': 'An inventory item', 'description': 'An inventory item',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -2916,6 +2917,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
'discovered': False, 'discovered': False,
'part_id': '123456', 'part_id': '123456',
'serial': '123ABC', 'serial': '123ABC',
'status': InventoryItemStatusChoices.STATUS_ACTIVE,
'description': 'An inventory item', 'description': 'An inventory item',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -2927,10 +2929,10 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"device,name,parent", "device,name,parent,status",
"Device 1,Inventory Item 4,Inventory Item 1", "Device 1,Inventory Item 4,Inventory Item 1,active",
"Device 1,Inventory Item 5,Inventory Item 2", "Device 1,Inventory Item 5,Inventory Item 2,planned",
"Device 1,Inventory Item 6,Inventory Item 3", "Device 1,Inventory Item 6,Inventory Item 3,failed",
) )
cls.csv_update_data = ( cls.csv_update_data = (

View File

@ -1,14 +0,0 @@
from django.conf import settings
from django.contrib.admin import site as admin_site
from taggit.models import Tag
# Override default AdminSite attributes so we can avoid creating and
# registering our own class
admin_site.site_header = 'NetBox Administration'
admin_site.site_title = 'NetBox'
admin_site.site_url = '/{}'.format(settings.BASE_PATH)
admin_site.index_template = 'admin/index.html'
# Unregister the unused stock Tag model provided by django-taggit
admin_site.unregister(Tag)

View File

@ -39,8 +39,6 @@ REDIS = {
SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
DJANGO_ADMIN_ENABLED = True
DEFAULT_PERMISSIONS = {} DEFAULT_PERMISSIONS = {}
LOGGING = { LOGGING = {

View File

@ -3,13 +3,11 @@ from importlib import import_module
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.conf.urls import include from django.conf.urls import include
from django.contrib.admin.views.decorators import staff_member_required
from django.urls import path from django.urls import path
from django.utils.module_loading import import_string, module_has_submodule from django.utils.module_loading import import_string, module_has_submodule
from . import views from . import views
# Initialize URL base, API, and admin URL patterns for plugins
plugin_patterns = [] plugin_patterns = []
plugin_api_patterns = [ plugin_api_patterns = [
path('', views.PluginsAPIRootView.as_view(), name='api-root'), path('', views.PluginsAPIRootView.as_view(), name='api-root'),

View File

@ -110,7 +110,6 @@ DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
'users.delete_token': ({'user': '$user'},), 'users.delete_token': ({'user': '$user'},),
}) })
DEVELOPER = getattr(configuration, 'DEVELOPER', False) DEVELOPER = getattr(configuration, 'DEVELOPER', False)
DJANGO_ADMIN_ENABLED = getattr(configuration, 'DJANGO_ADMIN_ENABLED', False)
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs')) DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
EMAIL = getattr(configuration, 'EMAIL', {}) EMAIL = getattr(configuration, 'EMAIL', {})
EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', ( EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', (
@ -373,7 +372,6 @@ SERVER_EMAIL = EMAIL.get('FROM_EMAIL')
# #
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
@ -411,8 +409,6 @@ INSTALLED_APPS = [
] ]
if not DEBUG: if not DEBUG:
INSTALLED_APPS.remove('debug_toolbar') INSTALLED_APPS.remove('debug_toolbar')
if not DJANGO_ADMIN_ENABLED:
INSTALLED_APPS.remove('django.contrib.admin')
# Middleware # Middleware
MIDDLEWARE = [ MIDDLEWARE = [
@ -549,7 +545,6 @@ EXEMPT_EXCLUDE_MODELS = (
# All URLs starting with a string listed here are exempt from maintenance mode enforcement # All URLs starting with a string listed here are exempt from maintenance mode enforcement
MAINTENANCE_EXEMPT_PATHS = ( MAINTENANCE_EXEMPT_PATHS = (
f'/{BASE_PATH}admin/',
f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration
LOGIN_URL, LOGIN_URL,
LOGIN_REDIRECT_URL, LOGIN_REDIRECT_URL,

View File

@ -1,9 +0,0 @@
from django.contrib import admin
from netbox.admin import admin_site
from .models import DummyModel
@admin.register(DummyModel, site=admin_site)
class DummyModelAdmin(admin.ModelAdmin):
list_display = ('name', 'number')

View File

@ -36,12 +36,6 @@ class PluginTest(TestCase):
instance.delete() instance.delete()
self.assertIsNone(instance.pk) self.assertIsNone(instance.pk)
def test_admin(self):
# Test admin view URL resolution
url = reverse('admin:dummy_plugin_dummymodel_add')
self.assertEqual(url, '/admin/dummy_plugin/dummymodel/add/')
@override_settings(LOGIN_REQUIRED=False) @override_settings(LOGIN_REQUIRED=False)
def test_views(self): def test_views(self):

View File

@ -77,11 +77,6 @@ _patterns = [
path('api/plugins/', include((plugin_api_patterns, 'plugins-api'))), path('api/plugins/', include((plugin_api_patterns, 'plugins-api'))),
] ]
# Django admin UI
if settings.DJANGO_ADMIN_ENABLED:
from .admin import admin_site
_patterns.append(path('admin/', admin_site.urls))
# django-debug-toolbar # django-debug-toolbar
if settings.DEBUG: if settings.DEBUG:
import debug_toolbar import debug_toolbar

View File

@ -56,6 +56,10 @@
<th scope="row">{% trans "Asset Tag" %}</th> <th scope="row">{% trans "Asset Tag" %}</th>
<td>{{ object.asset_tag|placeholder }}</td> <td>{{ object.asset_tag|placeholder }}</td>
</tr> </tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr> <tr>
<th scope="row">{% trans "Description" %}</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>

View File

@ -40,6 +40,16 @@
<th scope="row">{% trans "Description" %}</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
<tr>
<th scope="row">{% trans "Color" %}</th>
<td>
{% if object.color %}
<span class="badge color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr> <tr>
<th scope="row">{% trans "Power Port" %}</th> <th scope="row">{% trans "Power Port" %}</th>
<td>{{ object.power_port|linkify|placeholder }}</td> <td>{{ object.power_port|linkify|placeholder }}</td>

View File

@ -36,11 +36,6 @@
</div> </div>
</a> </a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow" {% htmx_boost %}> <div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow" {% htmx_boost %}>
{% if config.DJANGO_ADMIN_ENABLED and request.user.is_staff %}
<a class="dropdown-item" href="{% url 'admin:index' %}">
<i class="mdi mdi-cog"></i> {% trans "Django Admin" %}
</a>
{% endif %}
<a href="{% url 'account:profile' %}" class="dropdown-item"> <a href="{% url 'account:profile' %}" class="dropdown-item">
<i class="mdi mdi-account"></i> {% trans "Profile" %} <i class="mdi mdi-account"></i> {% trans "Profile" %}
</a> </a>

View File

@ -1,5 +0,0 @@
from django.contrib import admin
from django.contrib.auth.models import Group as DjangoGroup
# Prevent the stock Django Group model from appearing in the admin UI (if enabled)
admin.site.unregister(DjangoGroup)