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 (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
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']
|
||||
|
||||
|
||||
@admin.register(RackReservation)
|
||||
class RackRackReservationAdmin(admin.ModelAdmin):
|
||||
list_display = ['rack', 'units', 'description', 'user', 'created']
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
@ -19,7 +19,7 @@ from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||
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'))
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['units', 'description']
|
||||
|
||||
|
||||
#
|
||||
# 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 django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
#
|
||||
|
@ -29,6 +29,9 @@ urlpatterns = [
|
||||
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'),
|
||||
|
||||
# Rack reservations
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||
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+)/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<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
|
||||
|
||||
# Manufacturers
|
||||
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,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||
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()
|
||||
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', {
|
||||
'rack': rack,
|
||||
'reservations': reservations,
|
||||
'reserved_units': reserved_units,
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
@ -317,6 +325,24 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
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
|
||||
#
|
||||
|
@ -264,6 +264,15 @@ ul.rack_far_face li.blocked {
|
||||
#ffc7c7 14px
|
||||
);
|
||||
}
|
||||
ul.rack_near_face li.reserved {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
#f7f7f7,
|
||||
#f7f7f7 7px,
|
||||
#c7c7ff 7px,
|
||||
#c7c7ff 14px
|
||||
);
|
||||
}
|
||||
ul.rack_near_face {
|
||||
z-index: 200;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
{% load helpers %}
|
||||
|
||||
<ul class="rack_legend">
|
||||
{% for u in rack.units %}
|
||||
<li>{{ u }}</li>
|
||||
@ -35,9 +37,14 @@
|
||||
{% endifequal %}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="available">
|
||||
<li class="available{% if u.id in reserved_units.keys %} reserved{% endif %}">
|
||||
{% 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 %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
@ -189,6 +189,41 @@
|
||||
{% endif %}
|
||||
</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 class="row col-md-6">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
|
@ -27,6 +27,14 @@ def getlist(value, 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)
|
||||
def gfm(value):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user