mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 21:16:27 -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
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
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']
|
||||
template_name = 'circuits/circuittermination_edit.html'
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'circuit' in kwargs:
|
||||
obj.circuit = get_object_or_404(Circuit, pk=kwargs['circuit'])
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
if 'circuit' in url_kwargs:
|
||||
obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -5,8 +5,8 @@ from dcim.models import (
|
||||
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
||||
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Site,
|
||||
STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
||||
RACK_WIDTH_CHOICES, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||
)
|
||||
from extras.api.serializers import CustomFieldModelSerializer
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
@ -97,14 +97,13 @@ class NestedRackRoleSerializer(serializers.ModelSerializer):
|
||||
# Racks
|
||||
#
|
||||
|
||||
|
||||
class RackSerializer(CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer()
|
||||
tenant = NestedTenantSerializer()
|
||||
role = NestedRackRoleSerializer()
|
||||
type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
|
||||
width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
|
||||
# width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
|
||||
|
||||
class Meta:
|
||||
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
|
||||
#
|
||||
|
@ -16,6 +16,7 @@ router.register(r'sites', views.SiteViewSet)
|
||||
router.register(r'rack-groups', views.RackGroupViewSet)
|
||||
router.register(r'rack-roles', views.RackRoleViewSet)
|
||||
router.register(r'racks', views.RackViewSet)
|
||||
router.register(r'rack-reservations', views.RackReservationViewSet)
|
||||
|
||||
# Device types
|
||||
router.register(r'manufacturers', views.ManufacturerViewSet)
|
||||
|
@ -12,7 +12,8 @@ from django.shortcuts import get_object_or_404
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
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 extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
|
||||
@ -97,6 +98,16 @@ class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
return Response(elevation)
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationViewSet(ModelViewSet):
|
||||
queryset = RackReservation.objects.all()
|
||||
serializer_class = serializers.RackReservationSerializer
|
||||
filter_class = filters.RackReservationFilter
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
|
@ -9,7 +9,8 @@ from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
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):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, Q
|
||||
|
||||
@ -8,9 +9,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
||||
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
||||
SlugField,
|
||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
|
||||
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
||||
SmallTextarea, SlugField,
|
||||
)
|
||||
|
||||
from .formfields import MACAddressFormField
|
||||
@ -19,7 +20,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 +244,34 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
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
|
||||
#
|
||||
|
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 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,50 @@ 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.
|
||||
"""
|
||||
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
|
||||
#
|
||||
|
@ -136,6 +136,7 @@ class RackTest(APITestCase):
|
||||
'width',
|
||||
'u_height',
|
||||
'desc_units',
|
||||
'reservations',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
'front_units',
|
||||
|
@ -29,6 +29,10 @@ 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'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||
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+)/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,33 @@ 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, 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
|
||||
#
|
||||
@ -1517,9 +1552,9 @@ class ModuleEditView(PermissionRequiredMixin, ComponentEditView):
|
||||
model = Module
|
||||
form_class = forms.ModuleForm
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'device' in kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=kwargs['device'])
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
if 'device' in url_kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||
return obj
|
||||
|
||||
|
||||
|
@ -764,9 +764,9 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
form_class = forms.ServiceForm
|
||||
template_name = 'ipam/service_edit.html'
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'device' in kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=kwargs['device'])
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
if 'device' in url_kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ $(document).ready(function() {
|
||||
// Resolve child field by ID specified in parent
|
||||
var child_name = $(this).attr('filter-for');
|
||||
var child_field = $('#id_' + child_name);
|
||||
var child_selected = child_field.val();
|
||||
|
||||
// Wipe out any existing options within the child field
|
||||
child_field.empty();
|
||||
@ -106,7 +107,9 @@ $(document).ready(function() {
|
||||
$.each(response, function (index, choice) {
|
||||
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
|
||||
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);
|
||||
});
|
||||
|
@ -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,51 @@
|
||||
{% 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></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 class="row col-md-6">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
|
@ -169,6 +169,27 @@ class SelectWithDisabled(forms.Select):
|
||||
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):
|
||||
"""
|
||||
A select widget populated via an API call
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -145,9 +145,9 @@ class ObjectEditView(View):
|
||||
return get_object_or_404(self.model, pk=kwargs['pk'])
|
||||
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
|
||||
# given some parameter from the request URI.
|
||||
# given some parameter from the request URL.
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
@ -159,7 +159,7 @@ class ObjectEditView(View):
|
||||
def get(self, request, *args, **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}
|
||||
form = self.form_class(instance=obj, initial=initial_data)
|
||||
|
||||
@ -173,7 +173,7 @@ class ObjectEditView(View):
|
||||
def post(self, request, *args, **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)
|
||||
|
||||
if form.is_valid():
|
||||
|
Loading…
Reference in New Issue
Block a user