Closes #6874: Add tenant assignment for locations

This commit is contained in:
jeremystretch 2021-10-07 15:46:21 -04:00
parent 18c3bb673f
commit 5a6190e321
11 changed files with 82 additions and 19 deletions

View File

@ -6,6 +6,7 @@
### Enhancements ### Enhancements
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces * [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
* [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations
### Other Changes ### Other Changes

View File

@ -138,14 +138,15 @@ class LocationSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
parent = NestedLocationSerializer(required=False, allow_null=True) parent = NestedLocationSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Location model = Location
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created', 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'tenant', 'description', 'custom_fields',
'last_updated', 'rack_count', 'device_count', '_depth', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
] ]

View File

@ -148,13 +148,17 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
'site_id': '$site' 'site_id': '$site'
} }
) )
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField( description = forms.CharField(
max_length=200, max_length=200,
required=False required=False
) )
class Meta: class Meta:
nullable_fields = ['parent', 'description'] nullable_fields = ['parent', 'tenant', 'description']
class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):

View File

@ -120,10 +120,16 @@ class LocationCSVForm(CustomFieldModelCSVForm):
'invalid_choice': 'Location not found.', 'invalid_choice': 'Location not found.',
} }
) )
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Assigned tenant'
)
class Meta: class Meta:
model = Location model = Location
fields = ('site', 'parent', 'name', 'slug', 'description') fields = ('site', 'parent', 'name', 'slug', 'tenant', 'description')
class RackRoleCSVForm(CustomFieldModelCSVForm): class RackRoleCSVForm(CustomFieldModelCSVForm):

View File

@ -175,8 +175,13 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
tag = TagFilterField(model) tag = TagFilterField(model)
class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm): class LocationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = Location model = Location
field_groups = [
['q'],
['region_id', 'site_group_id', 'site_id', 'parent_id'],
['tenant_group_id', 'tenant_id'],
]
q = forms.CharField( q = forms.CharField(
required=False, required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),

View File

@ -157,7 +157,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
} }
class LocationForm(BootstrapMixin, CustomFieldModelForm): class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -191,7 +191,13 @@ class LocationForm(BootstrapMixin, CustomFieldModelForm):
class Meta: class Meta:
model = Location model = Location
fields = ( fields = (
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant',
)
fieldsets = (
('Location', (
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description',
)),
('Tenancy', ('tenant_group', 'tenant')),
) )

View File

@ -0,0 +1,18 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0002_tenant_ordering'),
('dcim', '0134_interface_wwn'),
]
operations = [
migrations.AddField(
model_name='location',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locations', to='tenancy.tenant'),
),
]

View File

@ -7,7 +7,6 @@ from timezone_field import TimeZoneField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from django.core.exceptions import ValidationError
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import NestedGroupModel, PrimaryModel from netbox.models import NestedGroupModel, PrimaryModel
@ -281,6 +280,13 @@ class Location(NestedGroupModel):
null=True, null=True,
db_index=True db_index=True
) )
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='locations',
blank=True,
null=True
)
description = models.CharField( description = models.CharField(
max_length=200, max_length=200,
blank=True blank=True

View File

@ -103,6 +103,7 @@ class LocationTable(BaseTable):
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
tenant = TenantColumn()
rack_count = LinkedCountColumn( rack_count = LinkedCountColumn(
viewname='dcim:rack_list', viewname='dcim:rack_list',
url_params={'location_id': 'pk'}, url_params={'location_id': 'pk'},
@ -120,5 +121,5 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Location model = Location
fields = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions') fields = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'actions')
default_columns = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'actions') default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'actions')

View File

@ -12,6 +12,7 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from ipam.models import VLAN from ipam.models import VLAN
from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_tags, create_test_device from utilities.testing import ViewTestCases, create_tags, create_test_device
@ -157,13 +158,13 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site = Site(name='Site 1', slug='site-1') site = Site.objects.create(name='Site 1', slug='site-1')
site.save() tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
locations = ( locations = (
Location(name='Location 1', slug='location-1', site=site), Location(name='Location 1', slug='location-1', site=site, tenant=tenant),
Location(name='Location 2', slug='location-2', site=site), Location(name='Location 2', slug='location-2', site=site, tenant=tenant),
Location(name='Location 3', slug='location-3', site=site), Location(name='Location 3', slug='location-3', site=site, tenant=tenant),
) )
for location in locations: for location in locations:
location.save() location.save()
@ -172,14 +173,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'name': 'Location X', 'name': 'Location X',
'slug': 'location-x', 'slug': 'location-x',
'site': site.pk, 'site': site.pk,
'tenant': tenant.pk,
'description': 'A new location', 'description': 'A new location',
} }
cls.csv_data = ( cls.csv_data = (
"site,name,slug,description", "site,tenant,name,slug,description",
"Site 1,Location 4,location-4,Fourth location", "Site 1,Tenant 1,Location 4,location-4,Fourth location",
"Site 1,Location 5,location-5,Fifth location", "Site 1,Tenant 1,Location 5,location-5,Fifth location",
"Site 1,Location 6,location-6,Sixth location", "Site 1,Tenant 1,Location 6,location-6,Sixth location",
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {

View File

@ -40,6 +40,19 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<th scope="row">Tenant</th>
<td>
{% if object.tenant %}
{% if object.tenant.group %}
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
{% endif %}
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<th scope="row">Racks</th> <th scope="row">Racks</th>
<td> <td>