Add a RackReservation.unit_count calculated field and filtering functionality

This commit is contained in:
Brian Tiemann
2026-03-12 20:28:47 -04:00
parent 02165a28a0
commit 4203ed9c5f
7 changed files with 63 additions and 12 deletions
+7 -2
View File
@@ -173,11 +173,16 @@ class RackReservationSerializer(PrimaryModelSerializer):
allow_null=True,
)
unit_count = serializers.SerializerMethodField()
def get_unit_count(self, obj):
return len(obj.units)
class Meta:
model = RackReservation
fields = [
'id', 'url', 'display_url', 'display', 'rack', 'units', 'status', 'created', 'last_updated', 'user',
'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields',
'id', 'url', 'display_url', 'display', 'rack', 'units', 'unit_count', 'status', 'created', 'last_updated',
'user', 'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields',
]
brief_fields = ('id', 'url', 'display', 'status', 'user', 'description', 'units')
+20
View File
@@ -1,6 +1,7 @@
import django_filters
import netaddr
from django.contrib.contenttypes.models import ContentType
from django.db.models import Func, IntegerField
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
@@ -609,11 +610,30 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
field_name='units',
lookup_expr='contains'
)
unit_count_min = django_filters.NumberFilter(
field_name='unit_count',
lookup_expr='gte',
label=_('Minimum unit count'),
)
unit_count_max = django_filters.NumberFilter(
field_name='unit_count',
lookup_expr='lte',
label=_('Maximum unit count'),
)
class Meta:
model = RackReservation
fields = ('id', 'created', 'description')
def filter_queryset(self, queryset):
# Annotate unit_count here so unit_count_min/unit_count_max filters can reference it.
# When called from the list view the queryset is already annotated; Django silently
# overwrites a duplicate annotation with the same expression, so this is safe.
queryset = queryset.annotate(
unit_count=Func('units', function='CARDINALITY', output_field=IntegerField())
)
return super().filter_queryset(queryset)
def search(self, queryset, name, value):
if not value.strip():
return queryset
+9 -1
View File
@@ -475,7 +475,7 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
model = RackReservation
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('status', 'user_id', name=_('Reservation')),
FieldSet('status', 'user_id', 'unit_count_min', 'unit_count_max', name=_('Reservation')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'group_id', 'rack_id', name=_('Rack')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
@@ -534,6 +534,14 @@ class RackReservationFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
required=False,
label=_('User')
)
unit_count_min = forms.IntegerField(
required=False,
label=_("Minimum U's")
)
unit_count_max = forms.IntegerField(
required=False,
label=_("Maximum U's")
)
tag = TagFilterField(model)
+8 -3
View File
@@ -241,6 +241,9 @@ class RackReservationTable(TenancyColumnsMixin, PrimaryModelTable):
orderable=False,
verbose_name=_('Units')
)
unit_count = tables.Column(
verbose_name=_("Total U's")
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
@@ -251,7 +254,9 @@ class RackReservationTable(TenancyColumnsMixin, PrimaryModelTable):
class Meta(PrimaryModelTable.Meta):
model = RackReservation
fields = (
'pk', 'id', 'reservation', 'site', 'location', 'group', 'rack', 'unit_list', 'status', 'user', 'created',
'tenant', 'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
'pk', 'id', 'reservation', 'site', 'location', 'group', 'rack', 'unit_list', 'unit_count', 'status',
'user', 'created', 'tenant', 'tenant_group', 'description', 'comments', 'tags', 'actions', 'last_updated',
)
default_columns = (
'pk', 'reservation', 'site', 'rack', 'unit_list', 'unit_count', 'status', 'user', 'description',
)
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'status', 'user', 'description')
+11 -3
View File
@@ -1205,7 +1205,7 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
reservations = (
RackReservation(
rack=racks[0],
units=[1, 2, 3],
units=[1, 2],
status=RackReservationStatusChoices.STATUS_ACTIVE,
user=users[0],
tenant=tenants[0],
@@ -1213,7 +1213,7 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
),
RackReservation(
rack=racks[1],
units=[4, 5, 6],
units=[1, 2, 3],
status=RackReservationStatusChoices.STATUS_PENDING,
user=users[1],
tenant=tenants[1],
@@ -1221,7 +1221,7 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
),
RackReservation(
rack=racks[2],
units=[7, 8, 9],
units=[1, 2, 3, 4],
status=RackReservationStatusChoices.STATUS_STALE,
user=users[2],
tenant=tenants[2],
@@ -1291,6 +1291,14 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_unit_count(self):
params = {'unit_count_min': 3}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'unit_count_max': 3}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'unit_count_min': 3, 'unit_count_max': 3}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
+1
View File
@@ -70,6 +70,7 @@ class RackRolePanel(panels.OrganizationalObjectPanel):
class RackReservationPanel(panels.ObjectAttributesPanel):
units = attrs.TextAttr('unit_list')
unit_count = attrs.TextAttr('unit_count', label=_("Total U's"))
status = attrs.ChoiceAttr('status')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
user = attrs.RelatedObjectAttr('user')
+7 -3
View File
@@ -3,7 +3,7 @@ from django.contrib import messages
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 Prefetch
from django.db.models import 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
@@ -1227,7 +1227,9 @@ class RackBulkDeleteView(generic.BulkDeleteView):
@register_model_view(RackReservation, 'list', path='', detail=False)
class RackReservationListView(generic.ObjectListView):
queryset = RackReservation.objects.all()
queryset = RackReservation.objects.annotate(
unit_count=Func('units', function='CARDINALITY', output_field=IntegerField())
)
filterset = filtersets.RackReservationFilterSet
filterset_form = forms.RackReservationFilterForm
table = tables.RackReservationTable
@@ -1236,7 +1238,9 @@ class RackReservationListView(generic.ObjectListView):
@register_model_view(RackReservation)
class RackReservationView(generic.ObjectView):
queryset = RackReservation.objects.all()
queryset = RackReservation.objects.annotate(
unit_count=Func('units', function='CARDINALITY', output_field=IntegerField())
)
layout = layout.SimpleLayout(
left_panels=[
panels.RackPanel(accessor='object.rack', only=['region', 'site', 'location', 'group', 'name']),