diff --git a/docs/models/dcim/rackreservation.md b/docs/models/dcim/rackreservation.md
index 32d52c9d7..8eaa11af8 100644
--- a/docs/models/dcim/rackreservation.md
+++ b/docs/models/dcim/rackreservation.md
@@ -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.
diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py
index 4bc2900dc..9c2c739fe 100644
--- a/netbox/dcim/api/serializers_/racks.py
+++ b/netbox/dcim/api/serializers_/racks.py
@@ -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):
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index ad96bd47c..81ea3c61a 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -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
#
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index b75febd72..4102f58d8 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -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)'),
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index 587b7dbde..b7d9bcdb7 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -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',)
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index be47f1fc0..94e2307e0 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -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)
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index 3c7a57546..daa3eef65 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -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,
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index bdaa1f0e3..02338a3b9 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -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',
]
diff --git a/netbox/dcim/migrations/0213_rackreservation_status.py b/netbox/dcim/migrations/0213_rackreservation_status.py
new file mode 100644
index 000000000..31822912c
--- /dev/null
+++ b/netbox/dcim/migrations/0213_rackreservation_status.py
@@ -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),
+ ),
+ ]
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index b15cd8b34..02bce2019 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -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
diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py
index ee40056de..afb2c44c8 100644
--- a/netbox/dcim/tables/racks.py
+++ b/netbox/dcim/tables/racks.py
@@ -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')
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index cefbc7b52..6a819a3c0 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -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',
},
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index f0701ee4b..357de1abc 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -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]}
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index 42a30e4f9..b23f7e16d 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -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',
diff --git a/netbox/templates/dcim/rackreservation.html b/netbox/templates/dcim/rackreservation.html
index f0f25dd83..87c4f7e4b 100644
--- a/netbox/templates/dcim/rackreservation.html
+++ b/netbox/templates/dcim/rackreservation.html
@@ -13,83 +13,87 @@
{% endblock %}
{% block content %}
-
-
-
-
-
-
- {% trans "Region" %} |
-
- {% nested_tree object.rack.site.region %}
- |
-
-
- {% trans "Site" %} |
- {{ object.rack.site|linkify }} |
-
-
- {% trans "Location" %} |
- {{ object.rack.location|linkify|placeholder }} |
-
-
- {% trans "Rack" %} |
- {{ object.rack|linkify }} |
-
-
-
-
-
-
-
- {% trans "Units" %} |
- {{ object.unit_list }} |
-
-
- {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- {% trans "User" %} |
- {{ object.user }} |
-
-
- {% trans "Description" %} |
- {{ object.description }} |
-
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_left_page object %}
-
-
+
+
+
+
+
+
+ {% trans "Region" %} |
+
+ {% nested_tree object.rack.site.region %}
+ |
+
+
+ {% trans "Site" %} |
+ {{ object.rack.site|linkify }} |
+
+
+ {% trans "Location" %} |
+ {{ object.rack.location|linkify|placeholder }} |
+
+
+ {% trans "Rack" %} |
+ {{ object.rack|linkify }} |
+
+
+
+
+
+
+
+ {% trans "Units" %} |
+ {{ object.unit_list }} |
+
+
+ {% trans "Status" %} |
+ {% badge object.get_status_display bg_color=object.get_status_color %} |
+
+
+ {% trans "Tenant" %} |
+
+ {% if object.tenant.group %}
+ {{ object.tenant.group|linkify }} /
+ {% endif %}
+ {{ object.tenant|linkify|placeholder }}
+ |
+
+
+ {% trans "User" %} |
+ {{ object.user }} |
+
+
+ {% trans "Description" %} |
+ {{ object.description }} |
+
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% include 'inc/panels/comments.html' %}
+ {% plugin_left_page object %}
+
+
-
-
-
{% trans "Front" %}
- {% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' %}
-
+
+
+
{% trans "Front" %}
+ {% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' %}
-
-
-
{% trans "Rear" %}
- {% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' %}
-
+
+
+
+
{% trans "Rear" %}
+ {% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' %}
+
{% plugin_right_page object %}
-
-
-
+
+
+
- {% plugin_full_width_page object %}
+ {% plugin_full_width_page object %}
-
+
{% endblock %}