Initial work on rack reservations

This commit is contained in:
Jeremy Stretch 2017-02-15 14:57:34 -05:00
parent b69564f5c9
commit 6b53570039
10 changed files with 164 additions and 5 deletions

View File

@ -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
# #

View File

@ -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
# #

View 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'],
},
),
]

View File

@ -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
# #

View File

@ -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'),

View File

@ -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
# #

View File

@ -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;
} }

View File

@ -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 }} &middot; {{ resv.created }}</small>"
{% endwith %}{% endif %}
>add device</a>
{% endif %} {% endif %}
</li> </li>
{% endif %} {% endif %}

View File

@ -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">

View File

@ -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):
""" """