diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md index 124de3037..576eb8739 100644 --- a/docs/configuration/miscellaneous.md +++ b/docs/configuration/miscellaneous.md @@ -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 !!! tip "Dynamic Configuration Parameter" diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md index b9029f75c..a6dfa32db 100644 --- a/docs/models/dcim/inventoryitem.md +++ b/docs/models/dcim/inventoryitem.md @@ -44,3 +44,7 @@ The serial number assigned by the manufacturer. ### Asset Tag A unique, locally-administered label used to identify hardware resources. + +### Status + +The inventory item's operational status. diff --git a/docs/models/dcim/poweroutlet.md b/docs/models/dcim/poweroutlet.md index 5c8bd6ff0..fe9390056 100644 --- a/docs/models/dcim/poweroutlet.md +++ b/docs/models/dcim/poweroutlet.md @@ -29,6 +29,10 @@ An alternative physical label identifying the power outlet. The type of power outlet. +### Color + +The power outlet's color (optional). + ### 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. diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py index 60d6561df..e285ce349 100644 --- a/netbox/dcim/api/serializers_/device_components.py +++ b/netbox/dcim/api/serializers_/device_components.py @@ -155,7 +155,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne class Meta: model = PowerOutlet 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', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', @@ -345,11 +345,12 @@ class InventoryItemSerializer(NetBoxModelSerializer): ) component = serializers.SerializerMethodField(read_only=True, allow_null=True) _depth = serializers.IntegerField(source='level', read_only=True) + status = ChoiceField(choices=InventoryItemStatusChoices, required=False) class Meta: model = InventoryItem 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', 'component', 'tags', 'custom_fields', 'created', 'last_updated', '_depth', ] diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 848f57d7e..89ca76870 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1648,3 +1648,27 @@ class VirtualDeviceContextStatusChoices(ChoiceSet): (STATUS_PLANNED, _('Planned'), 'cyan'), (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'), + ] diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 6517aadb4..d49e937cd 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1594,7 +1594,7 @@ class PowerOutletFilterSet( class Meta: model = PowerOutlet 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( lookup_expr='iexact' ) + status = django_filters.MultipleChoiceFilter( + choices=InventoryItemStatusChoices, + null_value=None + ) class Meta: 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): if not value.strip(): diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index 2939b986e..0ea3aee63 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/netbox/dcim/forms/bulk_create.py @@ -69,7 +69,7 @@ class PowerPortBulkCreateForm( class PowerOutletBulkCreateForm( - form_from_model(PowerOutlet, ['type', 'feed_leg', 'mark_connected']), + form_from_model(PowerOutlet, ['type', 'color', 'feed_leg', 'mark_connected']), DeviceBulkAddComponentForm ): model = PowerOutlet diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 96036f4da..01e5f138e 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1361,7 +1361,7 @@ class PowerPortBulkEditForm( class PowerOutletBulkEditForm( 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( label=_('Mark connected'), @@ -1371,7 +1371,7 @@ class PowerOutletBulkEditForm( model = PowerOutlet fieldsets = ( - FieldSet('module', 'type', 'label', 'description', 'mark_connected'), + FieldSet('module', 'type', 'label', 'description', 'mark_connected', 'color'), FieldSet('feed_leg', 'power_port', name=_('Power')), ) nullable_fields = ('module', 'label', 'type', 'feed_leg', 'power_port', 'description') @@ -1661,10 +1661,16 @@ class InventoryItemBulkEditForm( queryset=Manufacturer.objects.all(), required=False ) + status = forms.ChoiceField( + label=_('Status'), + choices=add_blank_choice(InventoryItemStatusChoices), + required=False, + initial='' + ) model = InventoryItem 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') diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index e9c8b362e..521e47a50 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -798,7 +798,7 @@ class PowerOutletImportForm(NetBoxModelImportForm): class Meta: 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): super().__init__(*args, **kwargs) @@ -1103,11 +1103,16 @@ class InventoryItemImportForm(NetBoxModelImportForm): required=False, help_text=_('Component Name') ) + status = CSVChoiceField( + label=_('Status'), + choices=InventoryItemStatusChoices, + help_text=_('Operational status') + ) class Meta: model = InventoryItem 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', ) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index e2b6fda07..10770824c 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -35,7 +35,6 @@ __all__ = ( 'LocationFilterForm', 'ManufacturerFilterForm', 'ModuleFilterForm', - 'ModuleFilterForm', 'ModuleBayFilterForm', 'ModuleTypeFilterForm', 'PlatformFilterForm', @@ -1304,7 +1303,7 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): model = PowerOutlet fieldsets = ( 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( 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', @@ -1318,6 +1317,10 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): required=False ) tag = TagFilterField(model) + color = ColorField( + label=_('Color'), + required=False + ) class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm): @@ -1553,6 +1556,11 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + status = forms.MultipleChoiceField( + label=_('Status'), + choices=InventoryItemStatusChoices, + required=False + ) tag = TagFilterField(model) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 4f08ea896..c22aeeea3 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1285,7 +1285,7 @@ class PowerOutletForm(ModularDeviceComponentForm): fieldsets = ( 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', ), ) @@ -1293,7 +1293,7 @@ class PowerOutletForm(ModularDeviceComponentForm): class Meta: model = PowerOutlet 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', ] @@ -1576,7 +1576,7 @@ class InventoryItemForm(DeviceComponentForm): ) 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( TabbedGroups( @@ -1596,7 +1596,7 @@ class InventoryItemForm(DeviceComponentForm): model = InventoryItem fields = [ 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', - 'description', 'tags', + 'status', 'description', 'tags', ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 24ba5cca4..b951aead0 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -568,6 +568,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): ) class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): power_port: Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')] | None + color: str @strawberry_django.type( diff --git a/netbox/dcim/migrations/0191_inventoryitem_status.py b/netbox/dcim/migrations/0191_inventoryitem_status.py new file mode 100644 index 000000000..23daf903d --- /dev/null +++ b/netbox/dcim/migrations/0191_inventoryitem_status.py @@ -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), + ), + ] diff --git a/netbox/dcim/migrations/0191_poweroutlet_color.py b/netbox/dcim/migrations/0191_poweroutlet_color.py new file mode 100644 index 000000000..6d03eecdc --- /dev/null +++ b/netbox/dcim/migrations/0191_poweroutlet_color.py @@ -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), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index f5fbaa956..54628aa73 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -481,6 +481,10 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki blank=True, help_text=_('Phase (for three-phase feeds)') ) + color = ColorField( + verbose_name=_('color'), + blank=True + ) clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg') @@ -1244,6 +1248,12 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin): ct_field='component_type', fk_field='component_id' ) + status = models.CharField( + verbose_name=_('status'), + max_length=50, + choices=InventoryItemStatusChoices, + default=InventoryItemStatusChoices.STATUS_ACTIVE + ) role = models.ForeignKey( to='dcim.InventoryItemRole', on_delete=models.PROTECT, @@ -1285,7 +1295,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin): objects = TreeManager() - clone_fields = ('device', 'parent', 'role', 'manufacturer', 'part_id',) + clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id') class Meta: ordering = ('device__id', 'parent__id', '_name') @@ -1334,3 +1344,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin): raise ValidationError({ "device": _("Cannot assign inventory item to component on another device") }) + + def get_status_color(self): + return InventoryItemStatusChoices.colors.get(self.status) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 2fa82dc43..380702b05 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -512,6 +512,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable): verbose_name=_('Power Port'), linkify=True ) + color = columns.ColorColumn() tags = columns.TagColumn( url_name='dcim:poweroutlet_list' ) @@ -520,10 +521,10 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable): model = models.PowerOutlet fields = ( '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', ) - 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): @@ -540,11 +541,11 @@ class DevicePowerOutletTable(PowerOutletTable): class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta): model = models.PowerOutlet fields = ( - 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description', - 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', + 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'color', 'power_port', 'feed_leg', + 'description', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions', ) 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'), false_mark=None ) + status = columns.ChoiceFieldColumn( + verbose_name=_('Status'), + ) parent = tables.Column( linkify=True, verbose_name=_('Parent'), @@ -958,11 +962,11 @@ class InventoryItemTable(DeviceComponentTable): class Meta(NetBoxTable.Meta): model = models.InventoryItem 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', ) 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): model = models.InventoryItem 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', ) 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', ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index afb360d76..b1fbcdcb5 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -3421,9 +3421,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF PowerPort.objects.bulk_create(power_ports) 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[1], module=modules[1], name='Power Outlet 2', label='B', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'), - PowerOutlet(device=devices[2], module=modules[2], name='Power Outlet 3', label='C', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'), + 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', color='00ff00'), + 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) @@ -3444,6 +3444,10 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF params = {'description': ['First', 'Second']} 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): params = {'feed_leg': [PowerOutletFeedLegChoices.FEED_LEG_A, PowerOutletFeedLegChoices.FEED_LEG_B]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -4751,9 +4755,9 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): ) 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[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[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[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, 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, status=ModuleStatusChoices.STATUS_FAILED, description='Third', component=components[2]), ) for i in inventory_items: i.save() @@ -4881,6 +4885,10 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'component_type': 'dcim.interface'} 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): queryset = InventoryItemRole.objects.all() diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 7d6c34337..a87d92642 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -2903,6 +2903,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): 'part_id': '123456', 'serial': '123ABC', 'asset_tag': 'ABC123', + 'status': InventoryItemStatusChoices.STATUS_ACTIVE, 'description': 'An inventory item', 'tags': [t.pk for t in tags], } @@ -2916,6 +2917,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): 'discovered': False, 'part_id': '123456', 'serial': '123ABC', + 'status': InventoryItemStatusChoices.STATUS_ACTIVE, 'description': 'An inventory item', 'tags': [t.pk for t in tags], } @@ -2927,10 +2929,10 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): } cls.csv_data = ( - "device,name,parent", - "Device 1,Inventory Item 4,Inventory Item 1", - "Device 1,Inventory Item 5,Inventory Item 2", - "Device 1,Inventory Item 6,Inventory Item 3", + "device,name,parent,status", + "Device 1,Inventory Item 4,Inventory Item 1,active", + "Device 1,Inventory Item 5,Inventory Item 2,planned", + "Device 1,Inventory Item 6,Inventory Item 3,failed", ) cls.csv_update_data = ( diff --git a/netbox/netbox/admin.py b/netbox/netbox/admin.py deleted file mode 100644 index cdfacc141..000000000 --- a/netbox/netbox/admin.py +++ /dev/null @@ -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) diff --git a/netbox/netbox/configuration_testing.py b/netbox/netbox/configuration_testing.py index 346cd89d2..cec05cabb 100644 --- a/netbox/netbox/configuration_testing.py +++ b/netbox/netbox/configuration_testing.py @@ -39,8 +39,6 @@ REDIS = { SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' -DJANGO_ADMIN_ENABLED = True - DEFAULT_PERMISSIONS = {} LOGGING = { diff --git a/netbox/netbox/plugins/urls.py b/netbox/netbox/plugins/urls.py index 075bda811..7a9f30c7e 100644 --- a/netbox/netbox/plugins/urls.py +++ b/netbox/netbox/plugins/urls.py @@ -3,13 +3,11 @@ from importlib import import_module from django.apps import apps from django.conf import settings from django.conf.urls import include -from django.contrib.admin.views.decorators import staff_member_required from django.urls import path from django.utils.module_loading import import_string, module_has_submodule from . import views -# Initialize URL base, API, and admin URL patterns for plugins plugin_patterns = [] plugin_api_patterns = [ path('', views.PluginsAPIRootView.as_view(), name='api-root'), diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 358f41ff8..206a58cff 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -110,7 +110,6 @@ DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', { 'users.delete_token': ({'user': '$user'},), }) 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')) EMAIL = getattr(configuration, 'EMAIL', {}) EVENTS_PIPELINE = getattr(configuration, 'EVENTS_PIPELINE', ( @@ -373,7 +372,6 @@ SERVER_EMAIL = EMAIL.get('FROM_EMAIL') # INSTALLED_APPS = [ - 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -411,8 +409,6 @@ INSTALLED_APPS = [ ] if not DEBUG: INSTALLED_APPS.remove('debug_toolbar') -if not DJANGO_ADMIN_ENABLED: - INSTALLED_APPS.remove('django.contrib.admin') # Middleware MIDDLEWARE = [ @@ -549,7 +545,6 @@ EXEMPT_EXCLUDE_MODELS = ( # All URLs starting with a string listed here are exempt from maintenance mode enforcement MAINTENANCE_EXEMPT_PATHS = ( - f'/{BASE_PATH}admin/', f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration LOGIN_URL, LOGIN_REDIRECT_URL, diff --git a/netbox/netbox/tests/dummy_plugin/admin.py b/netbox/netbox/tests/dummy_plugin/admin.py deleted file mode 100644 index 83bc22ad8..000000000 --- a/netbox/netbox/tests/dummy_plugin/admin.py +++ /dev/null @@ -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') diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py index 351fef9e2..ba44378c5 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -36,12 +36,6 @@ class PluginTest(TestCase): instance.delete() 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) def test_views(self): diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index b0175ec04..08c9a46a8 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -77,11 +77,6 @@ _patterns = [ 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 if settings.DEBUG: import debug_toolbar diff --git a/netbox/templates/dcim/inventoryitem.html b/netbox/templates/dcim/inventoryitem.html index 44648d53e..f17bf2ade 100644 --- a/netbox/templates/dcim/inventoryitem.html +++ b/netbox/templates/dcim/inventoryitem.html @@ -56,6 +56,10 @@