mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
Closes #6874: Add tenant assignment for locations
This commit is contained in:
parent
18c3bb673f
commit
5a6190e321
@ -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
|
||||||
|
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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')}),
|
||||||
|
@ -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')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
18
netbox/dcim/migrations/0135_location_tenant.py
Normal file
18
netbox/dcim/migrations/0135_location_tenant.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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 = {
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user