Browse Source

Add Tenancy to Rack Reservations; Fixes #1592 (#1672)

* fixed prefix header to represent new serial "vlan_vid"

* shows option in creation now

* fixed visibility on rack page

* cleanup

* Added view to Tenant page

* Moved migration for update from #1666 and fixed tenant enumeration in FilterForm

* Fixed conflict #1

* Fixed filters from merge and made migration merge

* added tenant to api

* Fixed migrations problem

* Added Tenant to bulkedit option
Nicholas Totsch 7 years ago
parent
commit
fbd39da8ca

+ 1 - 1
netbox/dcim/api/serializers.py

@@ -218,7 +218,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = RackReservation
-        fields = ['id', 'rack', 'units', 'created', 'user', 'description']
+        fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
 
 
 class WritableRackReservationSerializer(ValidatedModelSerializer):

+ 10 - 0
netbox/dcim/filters.py

@@ -208,6 +208,16 @@ class RackReservationFilter(django_filters.FilterSet):
         to_field_name='slug',
         label='Group',
     )
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=Tenant.objects.all(),
+        label='Tenant (ID)',
+    )
+    tenant = django_filters.ModelMultipleChoiceFilter(
+        name='tenant__slug',
+        queryset=Tenant.objects.all(),
+        to_field_name='slug',
+        label='Tenant (slug)',
+    )
     user_id = django_filters.ModelMultipleChoiceFilter(
         queryset=User.objects.all(),
         label='User (ID)',

+ 10 - 4
netbox/dcim/forms.py

@@ -379,13 +379,13 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
 # Rack reservations
 #
 
-class RackReservationForm(BootstrapMixin, forms.ModelForm):
+class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
     units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
     user = forms.ModelChoiceField(queryset=User.objects.order_by('username'))
 
     class Meta:
         model = RackReservation
-        fields = ['units', 'user', 'description']
+        fields = ['units', 'user', 'tenant_group', 'tenant', 'description']
 
     def __init__(self, *args, **kwargs):
 
@@ -415,11 +415,17 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
         label='Rack group',
         null_option=(0, 'None')
     )
+    tenant = FilterChoiceField(
+        queryset=Tenant.objects.annotate(filter_count=Count('rackreservations')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
 
 
 class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput)
     user = forms.ModelChoiceField(queryset=User.objects.order_by('username'), required=False)
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
     description = forms.CharField(max_length=100, required=False)
 
     class Meta:
@@ -805,10 +811,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
         pk = self.instance.pk if self.instance.pk else None
         try:
             if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
-                position_choices = Rack.objects.get(pk=self.data['rack'])\
+                position_choices = Rack.objects.get(pk=self.data['rack']) \
                     .get_rack_units(face=self.data.get('face'), exclude=pk)
             elif self.initial.get('rack') and str(self.initial.get('face')):
-                position_choices = Rack.objects.get(pk=self.initial['rack'])\
+                position_choices = Rack.objects.get(pk=self.initial['rack']) \
                     .get_rack_units(face=self.initial.get('face'), exclude=pk)
             else:
                 position_choices = []

+ 22 - 0
netbox/dcim/migrations/0050_rackreservation_tenant.py

@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.6 on 2017-10-30 20:43
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0003_unicode_literals'),
+        ('dcim', '0049_rackreservation_change_user'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='rackreservation',
+            name='tenant',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'),
+        ),
+    ]

+ 1 - 0
netbox/dcim/models.py

@@ -417,6 +417,7 @@ class RackReservation(models.Model):
     rack = models.ForeignKey('Rack', related_name='reservations', on_delete=models.CASCADE)
     units = ArrayField(models.PositiveSmallIntegerField())
     created = models.DateTimeField(auto_now_add=True)
+    tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='rackreservations', on_delete=models.PROTECT)
     user = models.ForeignKey(User, on_delete=models.PROTECT)
     description = models.CharField(max_length=100)
 

+ 2 - 1
netbox/dcim/tables.py

@@ -244,6 +244,7 @@ class RackImportTable(BaseTable):
 
 class RackReservationTable(BaseTable):
     pk = ToggleColumn()
+    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
     unit_list = tables.Column(orderable=False, verbose_name='Units')
     actions = tables.TemplateColumn(
@@ -252,7 +253,7 @@ class RackReservationTable(BaseTable):
 
     class Meta(BaseTable.Meta):
         model = RackReservation
-        fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'description', 'actions')
+        fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions')
 
 
 #

+ 4 - 4
netbox/dcim/views.py

@@ -326,7 +326,7 @@ class RackView(View):
 
         rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
 
-        nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
+        nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True) \
             .select_related('device_type__manufacturer')
         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()
@@ -1783,7 +1783,7 @@ class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView
 #
 
 class ConsoleConnectionsListView(ObjectListView):
-    queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False)\
+    queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False) \
         .order_by('cs_port__device__name', 'cs_port__name')
     filter = filters.ConsoleConnectionFilter
     filter_form = forms.ConsoleConnectionFilterForm
@@ -1792,7 +1792,7 @@ class ConsoleConnectionsListView(ObjectListView):
 
 
 class PowerConnectionsListView(ObjectListView):
-    queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False)\
+    queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False) \
         .order_by('power_outlet__device__name', 'power_outlet__name')
     filter = filters.PowerConnectionFilter
     filter_form = forms.PowerConnectionFilterForm
@@ -1801,7 +1801,7 @@ class PowerConnectionsListView(ObjectListView):
 
 
 class InterfaceConnectionsListView(ObjectListView):
-    queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')\
+    queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device') \
         .order_by('interface_a__device__name', 'interface_a__name')
     filter = filters.InterfaceConnectionFilter
     filter_form = forms.InterfaceConnectionFilterForm

+ 2 - 0
netbox/templates/dcim/rack.html

@@ -233,12 +233,14 @@
                 <table class="table table-hover panel-body">
                     <tr>
                         <th>Units</th>
+                        <th>Tenant</th>
                         <th>Description</th>
                         <th></th>
                     </tr>
                     {% for resv in reservations %}
                         <tr>
                             <td>{{ resv.unit_list }}</td>
+                            <td>{{ resv.tenant }}</td>
                             <td>
                                 {{ resv.description }}<br />
                                 <small>{{ resv.user }} &middot; {{ resv.created }}</small>

+ 4 - 0
netbox/templates/tenancy/tenant.html

@@ -101,6 +101,10 @@
                     <p>Racks</p>
                 </div>
                 <div class="col-md-4 text-center">
+                    <h2><a href="{% url 'dcim:rackreservation_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.rackreservation_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.rackreservation_count }}</a></h2>
+                    <p>Rack Reservations</p>
+                </div>
+                <div class="col-md-4 text-center">
                     <h2><a href="{% url 'dcim:device_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
                     <p>Devices</p>
                 </div>

+ 2 - 1
netbox/tenancy/views.py

@@ -7,7 +7,7 @@ from django.urls import reverse
 from django.views.generic import View
 
 from circuits.models import Circuit
-from dcim.models import Site, Rack, Device
+from dcim.models import Site, Rack, Device, RackReservation
 from ipam.models import IPAddress, Prefix, VLAN, VRF
 from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
@@ -75,6 +75,7 @@ class TenantView(View):
         stats = {
             'site_count': Site.objects.filter(tenant=tenant).count(),
             'rack_count': Rack.objects.filter(tenant=tenant).count(),
+            'rackreservation_count': RackReservation.objects.filter(tenant=tenant).count(),
             'device_count': Device.objects.filter(tenant=tenant).count(),
             'vrf_count': VRF.objects.filter(tenant=tenant).count(),
             'prefix_count': Prefix.objects.filter(