mirror of
https://github.com/netbox-community/netbox.git
synced 2025-09-06 14:23:36 -06:00
parent
032bd52dc7
commit
bb57021197
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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)'),
|
||||
|
@ -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',)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
16
netbox/dcim/migrations/0213_rackreservation_status.py
Normal file
16
netbox/dcim/migrations/0213_rackreservation_status.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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]}
|
||||
|
@ -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',
|
||||
|
@ -13,83 +13,87 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-xl-5">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Rack" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>
|
||||
{% nested_tree object.rack.site.region %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.rack.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{{ object.rack.location|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack" %}</th>
|
||||
<td>{{ object.rack|linkify }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Reservation Details" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Units" %}</th>
|
||||
<td>{{ object.unit_list }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "User" %}</th>
|
||||
<td>{{ object.user }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-xl-7">
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-xl-5">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Rack" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>
|
||||
{% nested_tree object.rack.site.region %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.rack.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{{ object.rack.location|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack" %}</th>
|
||||
<td>{{ object.rack|linkify }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Reservation Details" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<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>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "User" %}</th>
|
||||
<td>{{ object.user }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-xl-7">
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Front" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' %}
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h2 class="h4">{% trans "Front" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' %}
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: -30px">
|
||||
<h2 class="h4">{% trans "Rear" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: -30px">
|
||||
<h2 class="h4">{% trans "Rear" %}</h2>
|
||||
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user