From 6b535700391e4722f2cda6ebbeea714e336855d4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 15 Feb 2017 14:57:34 -0500 Subject: [PATCH] Initial work on rack reservations --- netbox/dcim/admin.py | 7 +++- netbox/dcim/forms.py | 13 ++++++- .../migrations/0026_add_rack_reservations.py | 34 ++++++++++++++++++ netbox/dcim/models.py | 20 +++++++++++ netbox/dcim/urls.py | 4 +++ netbox/dcim/views.py | 28 ++++++++++++++- netbox/project-static/css/base.css | 9 +++++ netbox/templates/dcim/inc/rack_elevation.html | 11 ++++-- netbox/templates/dcim/rack.html | 35 +++++++++++++++++++ netbox/utilities/templatetags/helpers.py | 8 +++++ 10 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 netbox/dcim/migrations/0026_add_rack_reservations.py diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index 8828b52c4..fb4c281ac 100644 --- a/netbox/dcim/admin.py +++ b/netbox/dcim/admin.py @@ -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 # diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9f6c7bde6..ed6b87a50 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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 # diff --git a/netbox/dcim/migrations/0026_add_rack_reservations.py b/netbox/dcim/migrations/0026_add_rack_reservations.py new file mode 100644 index 000000000..4d0b7a8a2 --- /dev/null +++ b/netbox/dcim/migrations/0026_add_rack_reservations.py @@ -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'], + }, + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index d29ca745d..7b8647418 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -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 # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 58bc91802..08f14617c 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -29,6 +29,9 @@ urlpatterns = [ url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), url(r'^rack-roles/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), + # Rack reservations + url(r'^rack-reservations/(?P\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\d+)/$', views.rack, name='rack'), url(r'^racks/(?P\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), + url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'), # Manufacturers url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 26226b79f..19a533339 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -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 # diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 4f569edea..11ea04b72 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -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; } diff --git a/netbox/templates/dcim/inc/rack_elevation.html b/netbox/templates/dcim/inc/rack_elevation.html index 0ffc6b7ad..049dcbc61 100644 --- a/netbox/templates/dcim/inc/rack_elevation.html +++ b/netbox/templates/dcim/inc/rack_elevation.html @@ -1,3 +1,5 @@ +{% load helpers %} +
    {% for u in rack.units %}
  • {{ u }}
  • @@ -35,9 +37,14 @@ {% endifequal %} {% else %} -
  • +
  • {% if perms.dcim.add_device %} - add device + add device {% endif %}
  • {% endif %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index d73a0b560..4f25e5c54 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -189,6 +189,41 @@ {% endif %} +
    +
    + Reservations +
    + {% if reservations %} + + + + + + + + + {% for resv in reservations %} + + + + + + + + {% endfor %} +
    UnitsDescriptionUserCreated
    {{ resv.units }}{{ resv.description }}{{ resv.user }}{{ resv.created }}{% if perms.change_rackreservation %}edit{% endif %}
    + {% else %} + None + {% endif %} + {% if perms.dcim.add_rackreservation %} + + {% endif %} +
    diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 74d490390..164aa24b2 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -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): """