Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/models/dcim/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ The [module bay](./modulebay.md) into which the module is installed.

The [module type](./moduletype.md) which represents the physical make & model of hardware. By default, module components will be instantiated automatically from the module type when creating a new module.

### Profile

The [module type profile](./moduletypeprofile.md) associated with the selected module type. Module list views and the REST API can be filtered by this related field.

Comment on lines +21 to +24
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not have been added. There is no profile field on the Module model.

### Status

The module's operational status.
Expand Down
13 changes: 13 additions & 0 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,19 @@ def _has_primary_ip(self, queryset, name, value):

@register_filterset
class ModuleFilterSet(PrimaryModelFilterSet):
profile_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_type__profile',
queryset=ModuleTypeProfile.objects.all(),
distinct=False,
label=_('Profile (ID)'),
)
profile = django_filters.ModelMultipleChoiceFilter(
field_name='module_type__profile__name',
queryset=ModuleTypeProfile.objects.all(),
distinct=False,
to_field_name='name',
label=_('Profile (name)'),
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='module_type__manufacturer',
queryset=Manufacturer.objects.all(),
Expand Down
14 changes: 13 additions & 1 deletion netbox/dcim/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
profile_id = DynamicModelMultipleChoiceField(
queryset=ModuleTypeProfile.objects.all(),
required=False,
null_option='None',
label=_('Profile')
)
manufacturer_id = DynamicModelMultipleChoiceField(
Expand Down Expand Up @@ -1040,9 +1041,13 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryM
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'device_id', name=_('Location')),
FieldSet('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag', name=_('Hardware')),
FieldSet(
'profile_id', 'manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag',
name=_('Hardware')
),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
)
selector_fields = ('filter_id', 'q', 'manufacturer_id', 'profile_id')
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
Expand Down Expand Up @@ -1095,10 +1100,17 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, PrimaryM
required=False,
label=_('Manufacturer')
)
profile_id = DynamicModelMultipleChoiceField(
queryset=ModuleTypeProfile.objects.all(),
required=False,
null_option='None',
label=_('Profile')
)
module_type_id = DynamicModelMultipleChoiceField(
queryset=ModuleType.objects.all(),
required=False,
query_params={
'profile_id': '$profile_id',
'manufacturer_id': '$manufacturer_id'
},
label=_('Type')
Expand Down
8 changes: 7 additions & 1 deletion netbox/dcim/tables/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ class ModuleTable(PrimaryModelTable):
accessor=tables.A('module_type__manufacturer'),
linkify=True
)
profile = tables.Column(
verbose_name=_('Profile'),
accessor=tables.A('module_type__profile'),
linkify=True,
)
module_type = tables.Column(
verbose_name=_('Module Type'),
linkify=True
Expand All @@ -107,7 +112,8 @@ class ModuleTable(PrimaryModelTable):
class Meta(PrimaryModelTable.Meta):
model = Module
fields = (
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag',
'pk', 'id', 'device', 'module_bay', 'manufacturer', 'profile', 'module_type', 'status',
'serial', 'asset_tag',
'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
Expand Down
44 changes: 41 additions & 3 deletions netbox/dcim/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

from django.conf import settings
from django.test import override_settings, tag
from django.urls import reverse
from django.utils.translation import gettext as _
Expand Down Expand Up @@ -1640,16 +1641,23 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'serial': '1234ABCD',
}
user_permissions = ('dcim.view_modulebay', 'dcim.view_moduletype', 'dcim.view_device')
user_permissions = (
'dcim.view_modulebay', 'dcim.view_moduletype', 'dcim.view_moduletypeprofile', 'dcim.view_device'
)

@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
profiles = (
ModuleTypeProfile(name='Test CPU'),
ModuleTypeProfile(name='Test Hard disk'),
)
ModuleTypeProfile.objects.bulk_create(profiles)
device = create_test_device('Test Device 1')

module_types = (
ModuleType(manufacturer=manufacturer, model='Module Type 1'),
ModuleType(manufacturer=manufacturer, model='Module Type 2'),
ModuleType(manufacturer=manufacturer, model='Module Type 1', profile=profiles[0]),
ModuleType(manufacturer=manufacturer, model='Module Type 2', profile=profiles[1]),
ModuleType(manufacturer=manufacturer, model='Module Type 3'),
)
ModuleType.objects.bulk_create(module_types)
Expand Down Expand Up @@ -1699,6 +1707,36 @@ def setUpTestData(cls):
},
]

def test_list_objects_by_profile_id(self):
profiles = ModuleTypeProfile.objects.filter(name__startswith='Test').order_by('name')
self.add_permissions('dcim.view_module')
response = self.client.get(self._get_list_url(), {'profile_id': [profiles[0].pk]}, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)

response = self.client.get(self._get_list_url(), {'profile_id': [profiles[1].pk]}, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)

response = self.client.get(
self._get_list_url(),
{'profile_id': [settings.FILTERS_NULL_CHOICE_VALUE]},
**self.header,
)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)

def test_list_objects_by_profile(self):
profiles = ModuleTypeProfile.objects.filter(name__startswith='Test').order_by('name')
self.add_permissions('dcim.view_module')
response = self.client.get(self._get_list_url(), {'profile': [profiles[0].name]}, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)

response = self.client.get(self._get_list_url(), {'profile': [profiles[1].name]}, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)


class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = ConsolePort
Expand Down
23 changes: 21 additions & 2 deletions netbox/dcim/tests/test_filtersets.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.conf import settings
from django.test import TestCase

from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
Expand Down Expand Up @@ -3085,6 +3086,11 @@ def setUpTestData(cls):
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
)
Manufacturer.objects.bulk_create(manufacturers)
module_type_profiles = (
ModuleTypeProfile(name='Test CPU'),
ModuleTypeProfile(name='Test Hard disk'),
)
ModuleTypeProfile.objects.bulk_create(module_type_profiles)

device_types = (
DeviceType(manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'),
Expand Down Expand Up @@ -3148,8 +3154,8 @@ def setUpTestData(cls):
Device.objects.bulk_create(devices)

module_types = (
ModuleType(manufacturer=manufacturers[0], model='Module Type 1'),
ModuleType(manufacturer=manufacturers[1], model='Module Type 2'),
ModuleType(manufacturer=manufacturers[0], model='Module Type 1', profile=module_type_profiles[0]),
ModuleType(manufacturer=manufacturers[1], model='Module Type 2', profile=module_type_profiles[1]),
ModuleType(manufacturer=manufacturers[2], model='Module Type 3'),
)
ModuleType.objects.bulk_create(module_types)
Expand Down Expand Up @@ -3265,6 +3271,19 @@ def test_module_type(self):
params = {'module_type': [module_types[0].model, module_types[1].model]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)

def test_profile(self):
profiles = ModuleTypeProfile.objects.filter(name__startswith='Test').order_by('name')
params = {'profile_id': [profiles[0].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'profile': [profiles[0].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'profile_id': [profiles[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'profile': [profiles[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
params = {'profile_id': [settings.FILTERS_NULL_CHOICE_VALUE]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)

def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
Expand Down
3 changes: 3 additions & 0 deletions netbox/dcim/tests/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ class ModuleTypeTableTest(TableTestCases.StandardTableTestCase):
class ModuleTableTest(TableTestCases.StandardTableTestCase):
table = ModuleTable

def test_profile_column_available(self):
self.assertIn('profile', self.table.base_columns)


#
# Devices
Expand Down
9 changes: 8 additions & 1 deletion netbox/dcim/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2424,13 +2424,14 @@ class ModuleTestCase(
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
module_type_profile = ModuleTypeProfile.objects.create(name='Module Type Profile 1')
devices = (
create_test_device('Device 1'),
create_test_device('Device 2'),
)

module_types = (
ModuleType(manufacturer=manufacturer, model='Module Type 1'),
ModuleType(manufacturer=manufacturer, model='Module Type 1', profile=module_type_profile),
ModuleType(manufacturer=manufacturer, model='Module Type 2'),
ModuleType(manufacturer=manufacturer, model='Module Type 3'),
ModuleType(manufacturer=manufacturer, model='Module Type 4'),
Expand Down Expand Up @@ -2489,6 +2490,12 @@ def setUpTestData(cls):
f"{modules[2].pk},offline,Serial 1",
)

@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_module_detail_includes_module_type_profile(self):
response = self.client.get(self._get_queryset().first().get_absolute_url())

self.assertContains(response, 'Module Type Profile 1')

@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_module_component_replication(self):
self.add_permissions('dcim.add_module')
Expand Down
4 changes: 4 additions & 0 deletions netbox/templates/dcim/panels/module_type.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<th scope="row">{% trans "Model" %}</th>
<td>{{ object.module_type|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Profile" %}</th>
<td>{{ object.module_type.profile|linkify|placeholder }}</td>
</tr>
{% for k, v in object.module_type.attributes.items %}
<tr>
<th scope="row">{{ k }}</th>
Expand Down
Loading