mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 11:56:25 -06:00
Initial work on rack reservations
This commit is contained in:
parent
b69564f5c9
commit
6b53570039
@ -4,7 +4,7 @@ from django.db.models import Count
|
|||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
|
||||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
|
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +37,11 @@ class RackAdmin(admin.ModelAdmin):
|
|||||||
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
|
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(RackReservation)
|
||||||
|
class RackRackReservationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['rack', 'units', 'description', 'user', 'created']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
@ -19,7 +19,7 @@ from .models import (
|
|||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||||
RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -243,6 +243,17 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
null_option=(0, 'None'))
|
null_option=(0, 'None'))
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = ['units', 'description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
34
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
34
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-15 19:57
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('dcim', '0025_devicetype_add_interface_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RackReservation',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1)]), size=None)),
|
||||||
|
('description', models.CharField(max_length=100)),
|
||||||
|
('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')),
|
||||||
|
('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['created'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,8 +1,10 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@ -478,6 +480,24 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
return int(float(self.u_height - u_available) / self.u_height * 100)
|
return int(float(self.u_height - u_available) / self.u_height * 100)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class RackReservation(models.Model):
|
||||||
|
"""
|
||||||
|
One or more reserved units within a Rack.
|
||||||
|
"""
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT)
|
||||||
|
rack = models.ForeignKey('Rack', related_name='reservations', editable=False, on_delete=models.CASCADE)
|
||||||
|
units = ArrayField(models.PositiveSmallIntegerField(validators=[MinValueValidator(1)]))
|
||||||
|
description = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['created']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return u"Reservation for rack {}".format(self.rack)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device Types
|
# Device Types
|
||||||
#
|
#
|
||||||
|
@ -29,6 +29,9 @@ urlpatterns = [
|
|||||||
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||||
|
|
||||||
|
# Rack reservations
|
||||||
|
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||||
|
|
||||||
# Racks
|
# Racks
|
||||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||||
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
|
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
|
||||||
@ -38,6 +41,7 @@ urlpatterns = [
|
|||||||
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
||||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||||
|
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
|
||||||
|
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||||
|
@ -26,7 +26,7 @@ from .models import (
|
|||||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackRole, Site,
|
RackReservation, RackRole, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -269,8 +269,16 @@ def rack(request, pk):
|
|||||||
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
|
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
|
||||||
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
||||||
|
|
||||||
|
reservations = RackReservation.objects.filter(rack=rack)
|
||||||
|
reserved_units = {}
|
||||||
|
for r in reservations:
|
||||||
|
for u in r.units:
|
||||||
|
reserved_units[u] = r
|
||||||
|
|
||||||
return render(request, 'dcim/rack.html', {
|
return render(request, 'dcim/rack.html', {
|
||||||
'rack': rack,
|
'rack': rack,
|
||||||
|
'reservations': reservations,
|
||||||
|
'reserved_units': reserved_units,
|
||||||
'nonracked_devices': nonracked_devices,
|
'nonracked_devices': nonracked_devices,
|
||||||
'next_rack': next_rack,
|
'next_rack': next_rack,
|
||||||
'prev_rack': prev_rack,
|
'prev_rack': prev_rack,
|
||||||
@ -317,6 +325,24 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
default_return_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'dcim.change_rackreservation'
|
||||||
|
model = RackReservation
|
||||||
|
form_class = forms.RackReservationForm
|
||||||
|
|
||||||
|
def alter_obj(self, obj, args, kwargs):
|
||||||
|
if 'rack' in kwargs:
|
||||||
|
obj.rack = get_object_or_404(Rack, pk=kwargs['rack'])
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.rack.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
@ -264,6 +264,15 @@ ul.rack_far_face li.blocked {
|
|||||||
#ffc7c7 14px
|
#ffc7c7 14px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
ul.rack_near_face li.reserved {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
#f7f7f7,
|
||||||
|
#f7f7f7 7px,
|
||||||
|
#c7c7ff 7px,
|
||||||
|
#c7c7ff 14px
|
||||||
|
);
|
||||||
|
}
|
||||||
ul.rack_near_face {
|
ul.rack_near_face {
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
<ul class="rack_legend">
|
<ul class="rack_legend">
|
||||||
{% for u in rack.units %}
|
{% for u in rack.units %}
|
||||||
<li>{{ u }}</li>
|
<li>{{ u }}</li>
|
||||||
@ -35,9 +37,14 @@
|
|||||||
{% endifequal %}
|
{% endifequal %}
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="available">
|
<li class="available{% if u.id in reserved_units.keys %} reserved{% endif %}">
|
||||||
{% if perms.dcim.add_device %}
|
{% if perms.dcim.add_device %}
|
||||||
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}&face={{ face_id }}&position={{ u.id }}" class="add_device" >add device</a>
|
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}&face={{ face_id }}&position={{ u.id }}" class="add_device"
|
||||||
|
{% if u.id in reserved_units.keys %}{% with reserved_units|getkey:u.id as resv %}
|
||||||
|
data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
|
||||||
|
data-content="{{ resv.description }}<br/><small>{{ resv.user }} · {{ resv.created }}</small>"
|
||||||
|
{% endwith %}{% endif %}
|
||||||
|
>add device</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -189,6 +189,41 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Reservations</strong>
|
||||||
|
</div>
|
||||||
|
{% if reservations %}
|
||||||
|
<table class="table table-hover panel-body">
|
||||||
|
<tr>
|
||||||
|
<th>Units</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
{% for resv in reservations %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ resv.units }}</td>
|
||||||
|
<td>{{ resv.description }}</td>
|
||||||
|
<td>{{ resv.user }}</td>
|
||||||
|
<td>{{ resv.created }}</td>
|
||||||
|
<td>{% if perms.change_rackreservation %}<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}">edit</a>{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.add_rackreservation %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'dcim:rack_add_reservation' rack=rack.pk %}" class="btn btn-primary btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
|
Add a reservation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row col-md-6">
|
<div class="row col-md-6">
|
||||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||||
|
@ -27,6 +27,14 @@ def getlist(value, arg):
|
|||||||
return value.getlist(arg)
|
return value.getlist(arg)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def getkey(value, key):
|
||||||
|
"""
|
||||||
|
Return a dictionary item specified by key
|
||||||
|
"""
|
||||||
|
return value[key]
|
||||||
|
|
||||||
|
|
||||||
@register.filter(is_safe=True)
|
@register.filter(is_safe=True)
|
||||||
def gfm(value):
|
def gfm(value):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user