Closes #18984: Add status field to Rack model

This commit is contained in:
Jeremy Stretch 2025-08-12 11:16:00 -04:00
parent 032bd52dc7
commit b0a6204a16
15 changed files with 230 additions and 96 deletions

View File

@ -12,6 +12,13 @@ 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.
### 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.)
!!! tip
Additional statuses may be defined by setting `RackReservation.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### User
The NetBox user account associated with the reservation. Note that users with sufficient permission can make rack reservations for other users.

View File

@ -137,17 +137,29 @@ class RackSerializer(RackBaseSerializer):
class RackReservationSerializer(NetBoxModelSerializer):
rack = RackSerializer(nested=True)
user = UserSerializer(nested=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
rack = RackSerializer(
nested=True,
)
status = ChoiceField(
choices=RackReservationStatusChoices,
required=False,
)
user = UserSerializer(
nested=True,
)
tenant = TenantSerializer(
nested=True,
required=False,
allow_null=True,
)
class Meta:
model = RackReservation
fields = [
'id', 'url', 'display_url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant',
'description', 'comments', 'tags', 'custom_fields',
'id', 'url', 'display_url', 'display', 'rack', 'units', 'status', 'created', 'last_updated', 'user',
'tenant', 'description', 'comments', 'tags', 'custom_fields',
]
brief_fields = ('id', 'url', 'display', 'user', 'description', 'units')
brief_fields = ('id', 'url', 'display', 'status', 'user', 'description', 'units')
class RackElevationDetailFilterSerializer(serializers.Serializer):

View File

@ -139,6 +139,24 @@ class RackAirflowChoices(ChoiceSet):
]
#
# Rack reservations
#
class RackReservationStatusChoices(ChoiceSet):
key = 'RackReservation.status'
STATUS_PENDING = 'pending'
STATUS_ACTIVE = 'active'
STATUS_STALE = 'stale'
CHOICES = [
(STATUS_PENDING, _('Pending'), 'cyan'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_STALE, _('Stale'), 'orange'),
]
#
# DeviceTypes
#

View File

@ -499,6 +499,10 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='slug',
label=_('Location (slug)'),
)
status = django_filters.MultipleChoiceFilter(
choices=RackReservationStatusChoices,
null_value=None
)
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label=_('User (ID)'),

View File

@ -476,6 +476,12 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(RackReservationStatusChoices),
required=False,
initial=''
)
user = forms.ModelChoiceField(
label=_('User'),
queryset=User.objects.order_by('username'),
@ -495,7 +501,7 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
model = RackReservation
fieldsets = (
FieldSet('user', 'tenant', 'description'),
FieldSet('status', 'user', 'tenant', 'description'),
)
nullable_fields = ('comments',)

View File

@ -358,6 +358,11 @@ class RackReservationImportForm(NetBoxModelImportForm):
required=True,
help_text=_('Comma-separated list of individual unit numbers')
)
status = CSVChoiceField(
label=_('Status'),
choices=RackReservationStatusChoices,
help_text=_('Operational status')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
@ -368,7 +373,7 @@ class RackReservationImportForm(NetBoxModelImportForm):
class Meta:
model = RackReservation
fields = ('site', 'location', 'rack', 'units', 'tenant', 'description', 'comments', 'tags')
fields = ('site', 'location', 'rack', 'units', 'status', 'tenant', 'description', 'comments', 'tags')
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)

View File

@ -417,7 +417,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RackReservation
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('user_id', name=_('User')),
FieldSet('status', 'user_id', name=_('Reservation')),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Rack')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
)
@ -458,6 +458,11 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
},
label=_('Rack')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=RackReservationStatusChoices,
required=False
)
user_id = DynamicModelMultipleChoiceField(
queryset=User.objects.all(),
required=False,

View File

@ -336,14 +336,14 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
FieldSet('rack', 'units', 'user', 'description', 'tags', name=_('Reservation')),
FieldSet('rack', 'units', 'status', 'user', 'description', 'tags', name=_('Reservation')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = RackReservation
fields = [
'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
]

View File

@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0212_platform_rebuild'),
]
operations = [
migrations.AddField(
model_name='rackreservation',
name='status',
field=models.CharField(default='active', max_length=50),
),
]

View File

@ -673,6 +673,12 @@ class RackReservation(PrimaryModel):
verbose_name=_('units'),
base_field=models.PositiveSmallIntegerField()
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=RackReservationStatusChoices,
default=RackReservationStatusChoices.STATUS_ACTIVE
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
@ -733,6 +739,9 @@ class RackReservation(PrimaryModel):
def unit_list(self):
return array_to_string(self.units)
def get_status_color(self):
return RackReservationStatusChoices.colors.get(self.status)
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.rack

View File

@ -229,6 +229,9 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name=_('Units')
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
@ -239,7 +242,7 @@ class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = RackReservation
fields = (
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'user', 'created', 'tenant',
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'created', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
)
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'status', 'user', 'description')

View File

@ -465,7 +465,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
class RackReservationTest(APIViewTestCases.APIViewTestCase):
model = RackReservation
brief_fields = ['description', 'display', 'id', 'units', 'url', 'user']
brief_fields = ['description', 'display', 'id', 'status', 'units', 'url', 'user']
bulk_update_data = {
'description': 'New description',
}
@ -483,9 +483,24 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
Rack.objects.bulk_create(racks)
rack_reservations = (
RackReservation(rack=racks[0], units=[1, 2, 3], user=user, description='Reservation #1'),
RackReservation(rack=racks[0], units=[4, 5, 6], user=user, description='Reservation #2'),
RackReservation(rack=racks[0], units=[7, 8, 9], user=user, description='Reservation #3'),
RackReservation(
rack=racks[0],
units=[1, 2, 3],
user=user,
description='Reservation #1',
),
RackReservation(
rack=racks[0],
units=[4, 5, 6],
user=user,
description='Reservation #2'
),
RackReservation(
rack=racks[0],
units=[7, 8, 9],
user=user,
description='Reservation #3',
),
)
RackReservation.objects.bulk_create(rack_reservations)
@ -493,18 +508,21 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
{
'rack': racks[1].pk,
'units': [10, 11, 12],
'status': RackReservationStatusChoices.STATUS_ACTIVE,
'user': user.pk,
'description': 'Reservation #4',
},
{
'rack': racks[1].pk,
'units': [13, 14, 15],
'status': RackReservationStatusChoices.STATUS_PENDING,
'user': user.pk,
'description': 'Reservation #5',
},
{
'rack': racks[1].pk,
'units': [16, 17, 18],
'status': RackReservationStatusChoices.STATUS_STALE,
'user': user.pk,
'description': 'Reservation #6',
},

View File

@ -1141,9 +1141,30 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
reservations = (
RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0], description='foobar1'),
RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1], description='foobar2'),
RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[2], description='foobar3'),
RackReservation(
rack=racks[0],
units=[1, 2, 3],
status=RackReservationStatusChoices.STATUS_ACTIVE,
user=users[0],
tenant=tenants[0],
description='foobar1',
),
RackReservation(
rack=racks[1],
units=[4, 5, 6],
status=RackReservationStatusChoices.STATUS_PENDING,
user=users[1],
tenant=tenants[1],
description='foobar2',
),
RackReservation(
rack=racks[2],
units=[7, 8, 9],
status=RackReservationStatusChoices.STATUS_STALE,
user=users[2],
tenant=tenants[2],
description='foobar3',
),
)
RackReservation.objects.bulk_create(reservations)
@ -1179,6 +1200,10 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_status(self):
params = {'status': [RackReservationStatusChoices.STATUS_ACTIVE, RackReservationStatusChoices.STATUS_PENDING]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_user(self):
users = User.objects.all()[:2]
params = {'user_id': [users[0].pk, users[1].pk]}

View File

@ -337,6 +337,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.form_data = {
'rack': rack.pk,
'units': "10,11,12",
'status': RackReservationStatusChoices.STATUS_PENDING,
'user': user3.pk,
'tenant': None,
'description': 'Rack reservation',
@ -344,10 +345,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
cls.csv_data = (
'site,location,rack,units,description',
'Site 1,Location 1,Rack 1,"10,11,12",Reservation 1',
'Site 1,Location 1,Rack 1,"13,14,15",Reservation 2',
'Site 1,Location 1,Rack 1,"16,17,18",Reservation 3',
'site,location,rack,units,status,description',
'Site 1,Location 1,Rack 1,"10,11,12",active,Reservation 1',
'Site 1,Location 1,Rack 1,"13,14,15",pending,Reservation 2',
'Site 1,Location 1,Rack 1,"16,17,18",stale,Reservation 3',
)
cls.csv_update_data = (
@ -358,6 +359,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
)
cls.bulk_edit_data = {
'status': RackReservationStatusChoices.STATUS_STALE,
'user': user3.pk,
'tenant': None,
'description': 'New description',

View File

@ -13,7 +13,7 @@
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="row mb-3">
<div class="col col-12 col-xl-5">
<div class="card">
<h2 class="card-header">{% trans "Rack" %}</h2>
@ -44,6 +44,10 @@
<tr>
<th scope="row">{% trans "Units" %}</th>
<td>{{ object.unit_list }}</td>
</tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>
@ -86,10 +90,10 @@
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
</div>
{% endblock %}