Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,6 @@ yarn-error.log*
/dist/
/build/
*.egg-info/

# Ruff's locally generated cache
.ruff_cache

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is out of scope for the PR, can we remove this

14 changes: 14 additions & 0 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,10 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
method='_inventory_items',
label=_('Has inventory items'),
)
has_images = django_filters.BooleanFilter(
method='_has_images',
label=('Has images'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Need _( internationalization here

)

class Meta:
model = DeviceType
Expand Down Expand Up @@ -794,6 +798,9 @@ def _device_bays(self, queryset, name, value):
def _inventory_items(self, queryset, name, value):
return queryset.exclude(inventoryitemtemplates__isnull=value)

def _has_images(self, queryset, name, value):
return queryset.exclude(images__isnull=value)


@register_filterset
class ModuleTypeProfileFilterSet(PrimaryModelFilterSet):
Expand Down Expand Up @@ -866,6 +873,10 @@ class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
method='_module_bays',
label=_('Has module bays'),
)
has_images = django_filters.BooleanFilter(
method='_has_images',
label=_('Has images'),
)

class Meta:
model = ModuleType
Expand Down Expand Up @@ -919,6 +930,9 @@ def _pass_through_ports(self, queryset, name, value):
def _module_bays(self, queryset, name, value):
return queryset.exclude(modulebaytemplates__isnull=value)

def _has_images(self, queryset, name, value):
return queryset.exclude(images__isnull=value)


class DeviceTypeComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter(
Expand Down
17 changes: 16 additions & 1 deletion netbox/dcim/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
'manufacturer_id', 'default_platform_id', 'part_number', 'device_count',
'subdevice_role', 'airflow', name=_('Hardware')
),
FieldSet('has_front_image', 'has_rear_image', name=_('Images')),
FieldSet('has_front_image', 'has_rear_image', 'has_images', name=_('Images')),
FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', name=_('Components')
Expand Down Expand Up @@ -615,6 +615,13 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
has_images = forms.NullBooleanField(
required=False,
label=_('Has images'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_ports = forms.NullBooleanField(
required=False,
label=_('Has console ports'),
Expand Down Expand Up @@ -712,6 +719,7 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'module_bays', name=_('Components')
),
FieldSet('has_images', name=_('Images')),
FieldSet('weight', 'weight_unit', name=_('Weight')),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
)
Expand Down Expand Up @@ -800,6 +808,13 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
choices=add_blank_choice(WeightUnitChoices),
required=False
)
has_images = forms.NullBooleanField(
required=False,
label=_('Has images'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)


class DeviceRoleFilterForm(NestedGroupModelFilterSetForm):
Expand Down
13 changes: 13 additions & 0 deletions netbox/dcim/tables/devicetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,26 @@ class DeviceTypeTable(PrimaryModelTable):
inventory_item_template_count = tables.Column(
verbose_name=_('Inventory Items')
)
front_image = columns.BooleanColumn(
verbose_name=_('Front Image'),
false_mark=None
)
rear_image = columns.BooleanColumn(
verbose_name=_('Rear Image'),
false_mark=None
)
image_count = tables.Column(
verbose_name=_('Images'),
orderable=False,
)

class Meta(PrimaryModelTable.Meta):
model = models.DeviceType
fields = (
'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height',
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
'description', 'comments', 'device_count', 'tags', 'created', 'last_updated',
'front_image', 'rear_image', 'image_count',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'device_count',
Expand Down
6 changes: 5 additions & 1 deletion netbox/dcim/tables/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,16 @@ class ModuleTypeTable(PrimaryModelTable):
tags = columns.TagColumn(
url_name='dcim:moduletype_list'
)
image_count = tables.Column(
verbose_name=_('Images'),
orderable=False,
)

class Meta(PrimaryModelTable.Meta):
model = ModuleType
fields = (
'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'description',
'attributes', 'module_count', 'comments', 'tags', 'created', 'last_updated',
'attributes', 'module_count', 'comments', 'tags', 'created', 'last_updated', 'image_count',
)
default_columns = (
'pk', 'model', 'profile', 'manufacturer', 'part_number', 'module_count',
Expand Down
34 changes: 33 additions & 1 deletion netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db import router, transaction
from django.db.models import Func, IntegerField, Prefetch
from django.db.models import Count, Func, IntegerField, Prefetch
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
Expand Down Expand Up @@ -1409,6 +1409,22 @@ class DeviceTypeListView(generic.ObjectListView):
filterset_form = forms.DeviceTypeFilterForm
table = tables.DeviceTypeTable

def get_table(self, data, request, bulk_actions=True):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is duplicated below, can we extract this out into a common place, maybe an ImageCountAnnotationMixin to keep it DRY

# Figures out which columns the user has picked before building the tables,
# because configure() tries to apply an existing sort, that may include image_count
# and it needs the existing annotation from the queryset.
if request.user.is_authenticated:
selected_columns = request.user.config.get(f'tables.{self.table.__name__}.columns')
else:
selected_columns = None
if selected_columns is None:
selected_columns = self.table.Meta.default_columns

if 'image_count' in selected_columns:
data = data.annotate(image_count=Count('images'))

return super().get_table(data, request, bulk_actions)


@register_model_view(DeviceType)
class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
Expand Down Expand Up @@ -1759,6 +1775,22 @@ class ModuleTypeListView(generic.ObjectListView):
filterset_form = forms.ModuleTypeFilterForm
table = tables.ModuleTypeTable

def get_table(self, data, request, bulk_actions=True):
# Figures out which columns the user has picked before building the tables,
# because configure() tries to apply an existing sort, that may include image_count
# and it needs the existing annotation from the queryset.
if request.user.is_authenticated:
selected_columns = request.user.config.get(f'tables.{self.table.__name__}.columns')
else:
selected_columns = None
if selected_columns is None:
selected_columns = self.table.Meta.default_columns

if 'image_count' in selected_columns:
data = data.annotate(image_count=Count('images'))

return super().get_table(data, request, bulk_actions)


@register_model_view(ModuleType)
class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
Expand Down