mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-20 19:19:22 -06:00
Merge branch 'develop' into api2
Conflicts: netbox/dcim/api/serializers.py netbox/dcim/api/urls.py netbox/dcim/api/views.py netbox/dcim/filters.py
This commit is contained in:
commit
b71566f206
@ -27,6 +27,12 @@ If you followed the original installation guide to set up gunicorn, be sure to c
|
|||||||
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Copy the LDAP configuration if using LDAP:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
|
||||||
|
```
|
||||||
|
|
||||||
## Option B: Clone the Git Repository (latest master release)
|
## Option B: Clone the Git Repository (latest master release)
|
||||||
|
|
||||||
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
||||||
|
@ -224,9 +224,9 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
fields_initial = ['term_side']
|
fields_initial = ['term_side']
|
||||||
template_name = 'circuits/circuittermination_edit.html'
|
template_name = 'circuits/circuittermination_edit.html'
|
||||||
|
|
||||||
def alter_obj(self, obj, args, kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
if 'circuit' in kwargs:
|
if 'circuit' in url_kwargs:
|
||||||
obj.circuit = get_object_or_404(Circuit, pk=kwargs['circuit'])
|
obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit'])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_return_url(self, obj):
|
def get_return_url(self, obj):
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -5,8 +5,8 @@ from dcim.models import (
|
|||||||
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
||||||
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||||
PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Site,
|
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
||||||
STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
RACK_WIDTH_CHOICES, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||||
)
|
)
|
||||||
from extras.api.serializers import CustomFieldModelSerializer
|
from extras.api.serializers import CustomFieldModelSerializer
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
@ -97,14 +97,13 @@ class NestedRackRoleSerializer(serializers.ModelSerializer):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class RackSerializer(CustomFieldModelSerializer):
|
class RackSerializer(CustomFieldModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
group = NestedRackGroupSerializer()
|
group = NestedRackGroupSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer()
|
||||||
role = NestedRackRoleSerializer()
|
role = NestedRackRoleSerializer()
|
||||||
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
|
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
|
||||||
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
|
# width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
@ -132,6 +131,18 @@ class WritableRackSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationSerializer(serializers.ModelSerializer):
|
||||||
|
rack = NestedRackSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
@ -16,6 +16,7 @@ router.register(r'sites', views.SiteViewSet)
|
|||||||
router.register(r'rack-groups', views.RackGroupViewSet)
|
router.register(r'rack-groups', views.RackGroupViewSet)
|
||||||
router.register(r'rack-roles', views.RackRoleViewSet)
|
router.register(r'rack-roles', views.RackRoleViewSet)
|
||||||
router.register(r'racks', views.RackViewSet)
|
router.register(r'racks', views.RackViewSet)
|
||||||
|
router.register(r'rack-reservations', views.RackReservationViewSet)
|
||||||
|
|
||||||
# Device types
|
# Device types
|
||||||
router.register(r'manufacturers', views.ManufacturerViewSet)
|
router.register(r'manufacturers', views.ManufacturerViewSet)
|
||||||
|
@ -12,7 +12,8 @@ from django.shortcuts import get_object_or_404
|
|||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
||||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
|
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||||
|
RackRole, Site,
|
||||||
)
|
)
|
||||||
from dcim import filters
|
from dcim import filters
|
||||||
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
|
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
|
||||||
@ -97,6 +98,16 @@ class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
return Response(elevation)
|
return Response(elevation)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationViewSet(ModelViewSet):
|
||||||
|
queryset = RackReservation.objects.all()
|
||||||
|
serializer_class = serializers.RackReservationSerializer
|
||||||
|
filter_class = filters.RackReservationFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
@ -9,7 +9,8 @@ from utilities.filters import NullableModelMultipleChoiceFilter
|
|||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
||||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
|
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||||
|
RackRole, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -123,6 +124,18 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationFilter(django_filters.FilterSet):
|
||||||
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='rack',
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
label='Rack (ID)',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = ['rack', 'user']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
q = django_filters.MethodFilter(
|
q = django_filters.MethodFilter(
|
||||||
action='search',
|
action='search',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
@ -8,9 +9,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
|||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
|
||||||
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
||||||
SlugField,
|
SmallTextarea, SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .formfields import MACAddressFormField
|
from .formfields import MACAddressFormField
|
||||||
@ -19,7 +20,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 +244,34 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
null_option=(0, 'None'))
|
null_option=(0, 'None'))
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackReservation
|
||||||
|
fields = ['units', 'description']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super(RackReservationForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Populate rack unit choices
|
||||||
|
self.fields['units'].widget.choices = self._get_unit_choices()
|
||||||
|
|
||||||
|
def _get_unit_choices(self):
|
||||||
|
rack = self.instance.rack
|
||||||
|
reserved_units = []
|
||||||
|
for resv in rack.reservations.exclude(pk=self.instance.pk):
|
||||||
|
for u in resv.units:
|
||||||
|
reserved_units.append(u)
|
||||||
|
unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
|
||||||
|
return unit_choices
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
33
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
33
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-16 18:43
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
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')),
|
||||||
|
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('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,50 @@ 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.
|
||||||
|
"""
|
||||||
|
rack = models.ForeignKey('Rack', related_name='reservations', editable=False, on_delete=models.CASCADE)
|
||||||
|
units = ArrayField(models.PositiveSmallIntegerField())
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT)
|
||||||
|
description = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['created']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return u"Reservation for rack {}".format(self.rack)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
if self.units:
|
||||||
|
|
||||||
|
# Validate that all specified units exist in the Rack.
|
||||||
|
invalid_units = [u for u in self.units if u not in self.rack.units]
|
||||||
|
if invalid_units:
|
||||||
|
raise ValidationError({
|
||||||
|
'units': u"Invalid unit(s) for {}U rack: {}".format(
|
||||||
|
self.rack.u_height,
|
||||||
|
', '.join([str(u) for u in invalid_units]),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check that none of the units has already been reserved for this Rack.
|
||||||
|
reserved_units = []
|
||||||
|
for resv in self.rack.reservations.exclude(pk=self.pk):
|
||||||
|
reserved_units += resv.units
|
||||||
|
conflicting_units = [u for u in self.units if u in reserved_units]
|
||||||
|
if conflicting_units:
|
||||||
|
raise ValidationError({
|
||||||
|
'units': 'The following units have already been reserved: {}'.format(
|
||||||
|
', '.join([str(u) for u in conflicting_units]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device Types
|
# Device Types
|
||||||
#
|
#
|
||||||
|
@ -136,6 +136,7 @@ class RackTest(APITestCase):
|
|||||||
'width',
|
'width',
|
||||||
'u_height',
|
'u_height',
|
||||||
'desc_units',
|
'desc_units',
|
||||||
|
'reservations',
|
||||||
'comments',
|
'comments',
|
||||||
'custom_fields',
|
'custom_fields',
|
||||||
'front_units',
|
'front_units',
|
||||||
|
@ -29,6 +29,10 @@ 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'),
|
||||||
|
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||||
|
|
||||||
# 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 +42,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,33 @@ 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, request, args, kwargs):
|
||||||
|
if not obj.pk:
|
||||||
|
obj.rack = get_object_or_404(Rack, pk=kwargs['rack'])
|
||||||
|
obj.user = request.user
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.rack.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_rackreservation'
|
||||||
|
model = RackReservation
|
||||||
|
|
||||||
|
def get_return_url(self, obj):
|
||||||
|
return obj.rack.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
@ -1517,9 +1552,9 @@ class ModuleEditView(PermissionRequiredMixin, ComponentEditView):
|
|||||||
model = Module
|
model = Module
|
||||||
form_class = forms.ModuleForm
|
form_class = forms.ModuleForm
|
||||||
|
|
||||||
def alter_obj(self, obj, args, kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
if 'device' in kwargs:
|
if 'device' in url_kwargs:
|
||||||
obj.device = get_object_or_404(Device, pk=kwargs['device'])
|
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@ -764,9 +764,9 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
form_class = forms.ServiceForm
|
form_class = forms.ServiceForm
|
||||||
template_name = 'ipam/service_edit.html'
|
template_name = 'ipam/service_edit.html'
|
||||||
|
|
||||||
def alter_obj(self, obj, args, kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
if 'device' in kwargs:
|
if 'device' in url_kwargs:
|
||||||
obj.device = get_object_or_404(Device, pk=kwargs['device'])
|
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_return_url(self, obj):
|
def get_return_url(self, obj):
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,7 @@ $(document).ready(function() {
|
|||||||
// Resolve child field by ID specified in parent
|
// Resolve child field by ID specified in parent
|
||||||
var child_name = $(this).attr('filter-for');
|
var child_name = $(this).attr('filter-for');
|
||||||
var child_field = $('#id_' + child_name);
|
var child_field = $('#id_' + child_name);
|
||||||
|
var child_selected = child_field.val();
|
||||||
|
|
||||||
// Wipe out any existing options within the child field
|
// Wipe out any existing options within the child field
|
||||||
child_field.empty();
|
child_field.empty();
|
||||||
@ -106,7 +107,9 @@ $(document).ready(function() {
|
|||||||
$.each(response, function (index, choice) {
|
$.each(response, function (index, choice) {
|
||||||
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
|
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
|
||||||
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
|
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
|
||||||
option.attr("disabled", "disabled")
|
option.attr("disabled", "disabled");
|
||||||
|
} else if (choice.id == child_selected) {
|
||||||
|
option.attr("selected", "selected");
|
||||||
}
|
}
|
||||||
child_field.append(option);
|
child_field.append(option);
|
||||||
});
|
});
|
||||||
|
@ -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,51 @@
|
|||||||
{% 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></th>
|
||||||
|
</tr>
|
||||||
|
{% for resv in reservations %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ resv.units|join:', ' }}</td>
|
||||||
|
<td>
|
||||||
|
{{ resv.description }}<br />
|
||||||
|
<small>{{ resv.user }} · {{ resv.created }}</small>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{% if perms.change_rackreservation %}
|
||||||
|
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}" class="btn btn-warning btn-xs" title="Edit reservation">
|
||||||
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.delete_rackreservation %}
|
||||||
|
<a href="{% url 'dcim:rackreservation_delete' pk=resv.pk %}" class="btn btn-danger btn-xs" title="Delete reservation">
|
||||||
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="panel-body text-muted">None</div>
|
||||||
|
{% 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">
|
||||||
|
@ -169,6 +169,27 @@ class SelectWithDisabled(forms.Select):
|
|||||||
force_text(option_label))
|
force_text(option_label))
|
||||||
|
|
||||||
|
|
||||||
|
class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple):
|
||||||
|
"""
|
||||||
|
MultiSelect widgets for a SimpleArrayField. Choices must be populated on the widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.delimiter = kwargs.pop('delimiter', ',')
|
||||||
|
super(ArrayFieldSelectMultiple, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def render_options(self, selected_choices):
|
||||||
|
# Split the delimited string of values into a list
|
||||||
|
if selected_choices:
|
||||||
|
selected_choices = selected_choices.split(self.delimiter)
|
||||||
|
return super(ArrayFieldSelectMultiple, self).render_options(selected_choices)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
# Condense the list of selected choices into a delimited string
|
||||||
|
data = super(ArrayFieldSelectMultiple, self).value_from_datadict(data, files, name)
|
||||||
|
return self.delimiter.join(data)
|
||||||
|
|
||||||
|
|
||||||
class APISelect(SelectWithDisabled):
|
class APISelect(SelectWithDisabled):
|
||||||
"""
|
"""
|
||||||
A select widget populated via an API call
|
A select widget populated via an API call
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -145,9 +145,9 @@ class ObjectEditView(View):
|
|||||||
return get_object_or_404(self.model, pk=kwargs['pk'])
|
return get_object_or_404(self.model, pk=kwargs['pk'])
|
||||||
return self.model()
|
return self.model()
|
||||||
|
|
||||||
def alter_obj(self, obj, args, kwargs):
|
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||||
# Allow views to add extra info to an object before it is processed. For example, a parent object can be defined
|
# Allow views to add extra info to an object before it is processed. For example, a parent object can be defined
|
||||||
# given some parameter from the request URI.
|
# given some parameter from the request URL.
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_return_url(self, obj):
|
def get_return_url(self, obj):
|
||||||
@ -159,7 +159,7 @@ class ObjectEditView(View):
|
|||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
obj = self.get_object(kwargs)
|
obj = self.get_object(kwargs)
|
||||||
obj = self.alter_obj(obj, args, kwargs)
|
obj = self.alter_obj(obj, request, args, kwargs)
|
||||||
initial_data = {k: request.GET[k] for k in self.fields_initial if k in request.GET}
|
initial_data = {k: request.GET[k] for k in self.fields_initial if k in request.GET}
|
||||||
form = self.form_class(instance=obj, initial=initial_data)
|
form = self.form_class(instance=obj, initial=initial_data)
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ class ObjectEditView(View):
|
|||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
obj = self.get_object(kwargs)
|
obj = self.get_object(kwargs)
|
||||||
obj = self.alter_obj(obj, args, kwargs)
|
obj = self.alter_obj(obj, request, args, kwargs)
|
||||||
form = self.form_class(request.POST, instance=obj)
|
form = self.form_class(request.POST, instance=obj)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
Loading…
Reference in New Issue
Block a user