From fbd39da8ca6b2bec4634e7d0eff63ad697771102 Mon Sep 17 00:00:00 2001 From: Nicholas Totsch Date: Wed, 15 Nov 2017 12:54:49 -0600 Subject: [PATCH] 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 --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/filters.py | 10 +++++++++ netbox/dcim/forms.py | 14 ++++++++---- .../migrations/0050_rackreservation_tenant.py | 22 +++++++++++++++++++ netbox/dcim/models.py | 1 + netbox/dcim/tables.py | 3 ++- netbox/dcim/views.py | 8 +++---- netbox/templates/dcim/rack.html | 2 ++ netbox/templates/tenancy/tenant.html | 4 ++++ netbox/tenancy/views.py | 3 ++- 10 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 netbox/dcim/migrations/0050_rackreservation_tenant.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1f2f3b9ed..cbbde86ed 100644 --- a/netbox/dcim/api/serializers.py +++ b/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): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index e466723bd..c7f1992f3 100644 --- a/netbox/dcim/filters.py +++ b/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)', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index a8b4b1cf5..9d6306d4d 100644 --- a/netbox/dcim/forms.py +++ b/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 = [] diff --git a/netbox/dcim/migrations/0050_rackreservation_tenant.py b/netbox/dcim/migrations/0050_rackreservation_tenant.py new file mode 100644 index 000000000..39da00f1f --- /dev/null +++ b/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'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1993e9143..eb4cb9b28 100644 --- a/netbox/dcim/models.py +++ b/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) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 3d1e79360..061b3bd9d 100644 --- a/netbox/dcim/tables.py +++ b/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') # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index a24720bce..2f55d073d 100644 --- a/netbox/dcim/views.py +++ b/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 diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 05585348f..2a6bc2f47 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -233,12 +233,14 @@ + {% for resv in reservations %} +
UnitsTenant Description
{{ resv.unit_list }}{{ resv.tenant }} {{ resv.description }}
{{ resv.user }} · {{ resv.created }} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index c19195246..e985ed8f9 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -100,6 +100,10 @@

{{ stats.rack_count }}

Racks

+
+

{{ stats.rackreservation_count }}

+

Rack Reservations

+

{{ stats.device_count }}

Devices

diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 33df6a5ca..99c4acc8a 100644 --- a/netbox/tenancy/views.py +++ b/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(