This commit is contained in:
parent
bc66d9f136
commit
2a78c05984
|
|
@ -12,6 +12,10 @@ The [rack](./rack.md) being reserved.
|
|||
|
||||
The rack unit or units being reserved. Multiple units can be expressed using commas and/or hyphens. For example, `1,3,5-7` specifies units 1, 3, 5, 6, and 7.
|
||||
|
||||
### Total U's
|
||||
|
||||
A calculated (read-only) field that reflects the total number of units in the reservation. Can be filtered upon using `unit_count_min` and `unit_count_max` parameters in the UI or API.
|
||||
|
||||
### Status
|
||||
|
||||
The current status of the reservation. (This is for documentation only: The status of a reservation has no impact on the installation of devices within a reserved rack unit.)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -606,11 +607,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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -997,6 +997,7 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
|
|||
units: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
unit_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_id: ID | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Annotated
|
|||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from django.db.models import Func, IntegerField
|
||||
|
||||
from core.graphql.mixins import ChangelogMixin
|
||||
from dcim import models
|
||||
|
|
@ -803,6 +804,17 @@ class RackReservationType(PrimaryObjectType):
|
|||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
user: Annotated["UserType", strawberry.lazy('users.graphql.types')]
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, queryset, info, **kwargs):
|
||||
queryset = super().get_queryset(queryset, info, **kwargs)
|
||||
return queryset.annotate(
|
||||
unit_count=Func('units', function='CARDINALITY', output_field=IntegerField())
|
||||
)
|
||||
|
||||
@strawberry.field
|
||||
def unit_count(self) -> int:
|
||||
return len(self.units)
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.RackRole,
|
||||
|
|
|
|||
|
|
@ -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', 'tenant',
|
||||
'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
|
||||
'pk', 'id', 'reservation', 'site', 'location', 'group', 'rack', 'unit_list', 'unit_count', 'status',
|
||||
'user', 'tenant', 'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', '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')
|
||||
|
|
|
|||
|
|
@ -570,6 +570,15 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
|
|||
},
|
||||
]
|
||||
|
||||
def test_unit_count(self):
|
||||
"""unit_count should reflect the number of units in the reservation."""
|
||||
url = reverse('dcim-api:rackreservation-list')
|
||||
self.add_permissions('dcim.view_rackreservation')
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertHttpStatus(response, 200)
|
||||
for result in response.data['results']:
|
||||
self.assertEqual(result['unit_count'], len(result['units']))
|
||||
|
||||
|
||||
class ManufacturerTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Manufacturer
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
|
|
@ -1289,7 +1293,9 @@ class RackReservationImportView(generic.BulkImportView):
|
|||
|
||||
@register_model_view(RackReservation, 'bulk_edit', path='edit', detail=False)
|
||||
class RackReservationBulkEditView(generic.BulkEditView):
|
||||
queryset = RackReservation.objects.all()
|
||||
queryset = RackReservation.objects.annotate(
|
||||
unit_count=Func('units', function='CARDINALITY', output_field=IntegerField())
|
||||
)
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
table = tables.RackReservationTable
|
||||
form = forms.RackReservationBulkEditForm
|
||||
|
|
@ -1297,7 +1303,9 @@ class RackReservationBulkEditView(generic.BulkEditView):
|
|||
|
||||
@register_model_view(RackReservation, 'bulk_delete', path='delete', detail=False)
|
||||
class RackReservationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackReservation.objects.all()
|
||||
queryset = RackReservation.objects.annotate(
|
||||
unit_count=Func('units', function='CARDINALITY', output_field=IntegerField())
|
||||
)
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
table = tables.RackReservationTable
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue