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
* [#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

View File

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

View File

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

View File

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

View File

@ -175,8 +175,13 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
tag = TagFilterField(model)
class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
class LocationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = Location
field_groups = [
['q'],
['region_id', 'site_group_id', 'site_id', 'parent_id'],
['tenant_group_id', 'tenant_id'],
]
q = forms.CharField(
required=False,
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(
queryset=Region.objects.all(),
required=False,
@ -191,7 +191,13 @@ class LocationForm(BootstrapMixin, CustomFieldModelForm):
class Meta:
model = Location
fields = (
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description',
'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant',
)
fieldsets = (
('Location', (
'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.constants import *
from django.core.exceptions import ValidationError
from dcim.fields import ASNField
from extras.utils import extras_features
from netbox.models import NestedGroupModel, PrimaryModel
@ -281,6 +280,13 @@ class Location(NestedGroupModel):
null=True,
db_index=True
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='locations',
blank=True,
null=True
)
description = models.CharField(
max_length=200,
blank=True

View File

@ -103,6 +103,7 @@ class LocationTable(BaseTable):
site = tables.Column(
linkify=True
)
tenant = TenantColumn()
rack_count = LinkedCountColumn(
viewname='dcim:rack_list',
url_params={'location_id': 'pk'},
@ -120,5 +121,5 @@ class LocationTable(BaseTable):
class Meta(BaseTable.Meta):
model = Location
fields = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'slug', 'actions')
default_columns = ('pk', 'name', 'site', 'rack_count', 'device_count', 'description', 'actions')
fields = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', '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.models import *
from ipam.models import VLAN
from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_tags, create_test_device
@ -157,13 +158,13 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@classmethod
def setUpTestData(cls):
site = Site(name='Site 1', slug='site-1')
site.save()
site = Site.objects.create(name='Site 1', slug='site-1')
tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')
locations = (
Location(name='Location 1', slug='location-1', site=site),
Location(name='Location 2', slug='location-2', site=site),
Location(name='Location 3', slug='location-3', site=site),
Location(name='Location 1', slug='location-1', site=site, tenant=tenant),
Location(name='Location 2', slug='location-2', site=site, tenant=tenant),
Location(name='Location 3', slug='location-3', site=site, tenant=tenant),
)
for location in locations:
location.save()
@ -172,14 +173,15 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
'name': 'Location X',
'slug': 'location-x',
'site': site.pk,
'tenant': tenant.pk,
'description': 'A new location',
}
cls.csv_data = (
"site,name,slug,description",
"Site 1,Location 4,location-4,Fourth location",
"Site 1,Location 5,location-5,Fifth location",
"Site 1,Location 6,location-6,Sixth location",
"site,tenant,name,slug,description",
"Site 1,Tenant 1,Location 4,location-4,Fourth location",
"Site 1,Tenant 1,Location 5,location-5,Fifth location",
"Site 1,Tenant 1,Location 6,location-6,Sixth location",
)
cls.bulk_edit_data = {

View File

@ -40,6 +40,19 @@
{% endif %}
</td>
</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>
<th scope="row">Racks</th>
<td>