Просмотр исходного кода

Closes #5895: Rename RackGroup to Location

Jeremy Stretch 5 лет назад
Родитель
Сommit
fdb3e3f9a4

+ 7 - 0
docs/release-notes/version-2.11.md

@@ -19,6 +19,7 @@ In addition to the new `mark_connected` boolean field, the REST API representati
 * [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models
 * [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
 * [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
+* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
 * [#5901](https://github.com/netbox-community/netbox/issues/5901) - Add `created` and `last_updated` fields to device component models
 
 ### Other Changes
@@ -39,5 +40,11 @@ In addition to the new `mark_connected` boolean field, the REST API representati
 * All cable termination models (cabled device components, power feeds, and circuit terminations)
   * Added `mark_connected` boolean field to force connection status
   * Added `_occupied` read-only boolean field as common attribute for determining whether an object is occupied
+* Renamed RackGroup to Location
+  * The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
+* dcim.PowerPanel
+  * Renamed `rack_group` field to `location`
+* dcim.Rack
+  * Renamed `group` field to `location`
 * extras.CustomField
   * Added new custom field type: `multi-select`

+ 4 - 4
netbox/dcim/api/nested_serializers.py

@@ -27,7 +27,7 @@ __all__ = [
     'NestedPowerPanelSerializer',
     'NestedPowerPortSerializer',
     'NestedPowerPortTemplateSerializer',
-    'NestedRackGroupSerializer',
+    'NestedLocationSerializer',
     'NestedRackReservationSerializer',
     'NestedRackRoleSerializer',
     'NestedRackSerializer',
@@ -65,13 +65,13 @@ class NestedSiteSerializer(WritableNestedSerializer):
 # Racks
 #
 
-class NestedRackGroupSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
+class NestedLocationSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
     rack_count = serializers.IntegerField(read_only=True)
     _depth = serializers.IntegerField(source='level', read_only=True)
 
     class Meta:
-        model = models.RackGroup
+        model = models.Location
         fields = ['id', 'url', 'name', 'slug', 'rack_count', '_depth']
 
 

+ 16 - 15
netbox/dcim/api/serializers.py

@@ -10,7 +10,7 @@ from dcim.models import (
     Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
     Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
-    PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
+    PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
     VirtualChassis,
 )
 from netbox.api.serializers import CustomFieldModelSerializer
@@ -121,14 +121,14 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
 # Racks
 #
 
-class RackGroupSerializer(NestedGroupModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
+class LocationSerializer(NestedGroupModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
     site = NestedSiteSerializer()
-    parent = NestedRackGroupSerializer(required=False, allow_null=True)
+    parent = NestedLocationSerializer(required=False, allow_null=True)
     rack_count = serializers.IntegerField(read_only=True)
 
     class Meta:
-        model = RackGroup
+        model = Location
         fields = [
             'id', 'url', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
             'rack_count', '_depth',
@@ -150,7 +150,7 @@ class RackRoleSerializer(OrganizationalModelSerializer):
 class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
     site = NestedSiteSerializer()
-    group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
+    location = NestedLocationSerializer(required=False, allow_null=True, default=None)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     status = ChoiceField(choices=RackStatusChoices, required=False)
     role = NestedRackRoleSerializer(required=False, allow_null=True)
@@ -163,21 +163,22 @@ class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
     class Meta:
         model = Rack
         fields = [
-            'id', 'url', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial',
-            'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
-            'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
+            'id', 'url', 'name', 'facility_id', 'display_name', 'site', 'location', 'tenant', 'status', 'role',
+            'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
+            'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
+            'powerfeed_count',
         ]
-        # Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
+        # Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This
         # prevents facility_id from being interpreted as a required field.
         validators = [
-            UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'name'))
+            UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'name'))
         ]
 
     def validate(self, data):
 
-        # Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta.
+        # Validate uniqueness of (location, facility_id) since we omitted the automatically-created validator from Meta.
         if data.get('facility_id', None):
-            validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id'))
+            validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'facility_id'))
             validator(data, self)
 
         # Enforce model validation
@@ -856,7 +857,7 @@ class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerialize
 class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
     site = NestedSiteSerializer()
-    rack_group = NestedRackGroupSerializer(
+    location = NestedLocationSerializer(
         required=False,
         allow_null=True,
         default=None
@@ -865,7 +866,7 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
 
     class Meta:
         model = PowerPanel
-        fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count']
+        fields = ['id', 'url', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
 
 
 class PowerFeedSerializer(

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

@@ -10,7 +10,7 @@ router.register('regions', views.RegionViewSet)
 router.register('sites', views.SiteViewSet)
 
 # Racks
-router.register('rack-groups', views.RackGroupViewSet)
+router.register('locations', views.LocationViewSet)
 router.register('rack-roles', views.RackRoleViewSet)
 router.register('racks', views.RackViewSet)
 router.register('rack-reservations', views.RackReservationViewSet)

+ 9 - 9
netbox/dcim/api/views.py

@@ -20,7 +20,7 @@ from dcim.models import (
     Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
     Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
-    PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
+    PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
     VirtualChassis,
 )
 from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
@@ -134,16 +134,16 @@ class SiteViewSet(CustomFieldModelViewSet):
 # Rack groups
 #
 
-class RackGroupViewSet(CustomFieldModelViewSet):
-    queryset = RackGroup.objects.add_related_count(
-        RackGroup.objects.all(),
+class LocationViewSet(CustomFieldModelViewSet):
+    queryset = Location.objects.add_related_count(
+        Location.objects.all(),
         Rack,
-        'group',
+        'location',
         'rack_count',
         cumulative=True
     ).prefetch_related('site')
-    serializer_class = serializers.RackGroupSerializer
-    filterset_class = filters.RackGroupFilterSet
+    serializer_class = serializers.LocationSerializer
+    filterset_class = filters.LocationFilterSet
 
 
 #
@@ -164,7 +164,7 @@ class RackRoleViewSet(CustomFieldModelViewSet):
 
 class RackViewSet(CustomFieldModelViewSet):
     queryset = Rack.objects.prefetch_related(
-        'site', 'group__site', 'role', 'tenant', 'tags'
+        'site', 'location__site', 'role', 'tenant', 'tags'
     ).annotate(
         device_count=count_related(Device, 'rack'),
         powerfeed_count=count_related(PowerFeed, 'rack')
@@ -619,7 +619,7 @@ class VirtualChassisViewSet(ModelViewSet):
 
 class PowerPanelViewSet(ModelViewSet):
     queryset = PowerPanel.objects.prefetch_related(
-        'site', 'rack_group'
+        'site', 'location'
     ).annotate(
         powerfeed_count=count_related(PowerFeed, 'power_panel')
     )

+ 29 - 29
netbox/dcim/filters.py

@@ -16,7 +16,7 @@ from .models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
     InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
-    PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
+    PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
     VirtualChassis,
 )
 
@@ -40,6 +40,7 @@ __all__ = (
     'InterfaceFilterSet',
     'InterfaceTemplateFilterSet',
     'InventoryItemFilterSet',
+    'LocationFilterSet',
     'ManufacturerFilterSet',
     'PathEndpointFilterSet',
     'PlatformFilterSet',
@@ -51,7 +52,6 @@ __all__ = (
     'PowerPortFilterSet',
     'PowerPortTemplateFilterSet',
     'RackFilterSet',
-    'RackGroupFilterSet',
     'RackReservationFilterSet',
     'RackRoleFilterSet',
     'RearPortFilterSet',
@@ -131,7 +131,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         return queryset.filter(qs_filter)
 
 
-class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
+class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         field_name='site__region',
@@ -156,18 +156,18 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
         label='Site (slug)',
     )
     parent_id = django_filters.ModelMultipleChoiceFilter(
-        queryset=RackGroup.objects.all(),
+        queryset=Location.objects.all(),
         label='Rack group (ID)',
     )
     parent = django_filters.ModelMultipleChoiceFilter(
         field_name='parent__slug',
-        queryset=RackGroup.objects.all(),
+        queryset=Location.objects.all(),
         to_field_name='slug',
         label='Rack group (slug)',
     )
 
     class Meta:
-        model = RackGroup
+        model = Location
         fields = ['id', 'name', 'slug', 'description']
 
 
@@ -206,18 +206,18 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
         to_field_name='slug',
         label='Site (slug)',
     )
-    group_id = TreeNodeMultipleChoiceFilter(
-        queryset=RackGroup.objects.all(),
-        field_name='group',
+    location_id = TreeNodeMultipleChoiceFilter(
+        queryset=Location.objects.all(),
+        field_name='location',
         lookup_expr='in',
-        label='Rack group (ID)',
+        label='Location (ID)',
     )
-    group = TreeNodeMultipleChoiceFilter(
-        queryset=RackGroup.objects.all(),
-        field_name='group',
+    location = TreeNodeMultipleChoiceFilter(
+        queryset=Location.objects.all(),
+        field_name='location',
         lookup_expr='in',
         to_field_name='slug',
-        label='Rack group (slug)',
+        label='Location (slug)',
     )
     status = django_filters.MultipleChoiceFilter(
         choices=RackStatusChoices,
@@ -283,18 +283,18 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
         to_field_name='slug',
         label='Site (slug)',
     )
-    group_id = TreeNodeMultipleChoiceFilter(
-        queryset=RackGroup.objects.all(),
-        field_name='rack__group',
+    location_id = TreeNodeMultipleChoiceFilter(
+        queryset=Location.objects.all(),
+        field_name='rack__location',
         lookup_expr='in',
-        label='Rack group (ID)',
+        label='Location (ID)',
     )
-    group = TreeNodeMultipleChoiceFilter(
-        queryset=RackGroup.objects.all(),
-        field_name='rack__group',
+    location = TreeNodeMultipleChoiceFilter(
+        queryset=Location.objects.all(),
+        field_name='rack__location',
         lookup_expr='in',
         to_field_name='slug',
-        label='Rack group (slug)',
+        label='Location (slug)',
     )
     user_id = django_filters.ModelMultipleChoiceFilter(
         queryset=User.objects.all(),
@@ -575,11 +575,11 @@ class DeviceFilterSet(
         to_field_name='slug',
         label='Site name (slug)',
     )
-    rack_group_id = TreeNodeMultipleChoiceFilter(
-        queryset=RackGroup.objects.all(),
-        field_name='rack__group',
+    location_id = TreeNodeMultipleChoiceFilter(
+        queryset=Location.objects.all(),
+        field_name='rack__location',
         lookup_expr='in',
-        label='Rack group (ID)',
+        label='Location (ID)',
     )
     rack_id = django_filters.ModelMultipleChoiceFilter(
         field_name='rack',
@@ -1236,9 +1236,9 @@ class PowerPanelFilterSet(BaseFilterSet):
         to_field_name='slug',
         label='Site name (slug)',
     )
-    rack_group_id = TreeNodeMultipleChoiceFilter(
-        queryset=RackGroup.objects.all(),
-        field_name='rack_group',
+    location_id = TreeNodeMultipleChoiceFilter(
+        queryset=Location.objects.all(),
+        field_name='location',
         lookup_expr='in',
         label='Rack group (ID)',
     )

+ 85 - 83
netbox/dcim/forms.py

@@ -35,7 +35,7 @@ from .models import (
     Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
     Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
     InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate,
-    Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
+    Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
 )
 
 DEVICE_BY_PK_RE = r'{\d+\}'
@@ -358,10 +358,10 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
 
 
 #
-# Rack groups
+# Locations
 #
 
-class RackGroupForm(BootstrapMixin, CustomFieldModelForm):
+class LocationForm(BootstrapMixin, CustomFieldModelForm):
     region = DynamicModelChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -376,7 +376,7 @@ class RackGroupForm(BootstrapMixin, CustomFieldModelForm):
         }
     )
     parent = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
+        queryset=Location.objects.all(),
         required=False,
         query_params={
             'site_id': '$site'
@@ -385,20 +385,20 @@ class RackGroupForm(BootstrapMixin, CustomFieldModelForm):
     slug = SlugField()
 
     class Meta:
-        model = RackGroup
+        model = Location
         fields = (
             'region', 'site', 'parent', 'name', 'slug', 'description',
         )
 
 
-class RackGroupCSVForm(CustomFieldModelCSVForm):
+class LocationCSVForm(CustomFieldModelCSVForm):
     site = CSVModelChoiceField(
         queryset=Site.objects.all(),
         to_field_name='name',
         help_text='Assigned site'
     )
     parent = CSVModelChoiceField(
-        queryset=RackGroup.objects.all(),
+        queryset=Location.objects.all(),
         required=False,
         to_field_name='name',
         help_text='Parent rack group',
@@ -408,11 +408,11 @@ class RackGroupCSVForm(CustomFieldModelCSVForm):
     )
 
     class Meta:
-        model = RackGroup
-        fields = RackGroup.csv_headers
+        model = Location
+        fields = Location.csv_headers
 
 
-class RackGroupFilterForm(BootstrapMixin, forms.Form):
+class LocationFilterForm(BootstrapMixin, forms.Form):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -427,7 +427,7 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form):
         label=_('Site')
     )
     parent = DynamicModelMultipleChoiceField(
-        queryset=RackGroup.objects.all(),
+        queryset=Location.objects.all(),
         required=False,
         query_params={
             'region_id': '$region_id',
@@ -480,8 +480,8 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             'region_id': '$region'
         }
     )
-    group = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = DynamicModelChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         query_params={
             'site_id': '$site'
@@ -500,7 +500,7 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     class Meta:
         model = Rack
         fields = [
-            'region', 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
+            'region', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
             'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
             'comments', 'tags',
         ]
@@ -523,8 +523,8 @@ class RackCSVForm(CustomFieldModelCSVForm):
         queryset=Site.objects.all(),
         to_field_name='name'
     )
-    group = CSVModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = CSVModelChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         to_field_name='name'
     )
@@ -569,9 +569,9 @@ class RackCSVForm(CustomFieldModelCSVForm):
 
         if data:
 
-            # Limit group queryset by assigned site
+            # Limit location queryset by assigned site
             params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
-            self.fields['group'].queryset = self.fields['group'].queryset.filter(**params)
+            self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
 
 
 class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
@@ -593,8 +593,8 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
             'region_id': '$region'
         }
     )
-    group = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = DynamicModelChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         query_params={
             'site_id': '$site'
@@ -662,13 +662,13 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
 
     class Meta:
         nullable_fields = [
-            'group', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
+            'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
         ]
 
 
 class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
     model = Rack
-    field_order = ['q', 'region_id', 'site_id', 'group_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
+    field_order = ['q', 'region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
     q = forms.CharField(
         required=False,
         label=_('Search')
@@ -686,14 +686,14 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
         },
         label=_('Site')
     )
-    group_id = DynamicModelMultipleChoiceField(
-        queryset=RackGroup.objects.all(),
+    location_id = DynamicModelMultipleChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         null_option='None',
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Rack group')
+        label=_('Location')
     )
     status = forms.MultipleChoiceField(
         choices=RackStatusChoices,
@@ -724,7 +724,9 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
 #
 
 class RackElevationFilterForm(RackFilterForm):
-    field_order = ['q', 'region_id', 'site_id', 'group_id', 'id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
+    field_order = [
+        'q', 'region_id', 'site_id', 'location_id', 'id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
+    ]
     id = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
         label=_('Rack'),
@@ -732,7 +734,7 @@ class RackElevationFilterForm(RackFilterForm):
         display_field='display_name',
         query_params={
             'site_id': '$site_id',
-            'group_id_id': '$group_id_id',
+            'location_id': '$location_id',
         }
     )
 
@@ -756,8 +758,8 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             'region_id': '$region'
         }
     )
-    rack_group = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = DynamicModelChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         query_params={
             'site_id': '$site'
@@ -768,7 +770,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         display_field='display_name',
         query_params={
             'site_id': '$site',
-            'group_id': '$rack_group',
+            'location_id': 'location',
         }
     )
     units = NumericArrayField(
@@ -789,10 +791,10 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     class Meta:
         model = RackReservation
         fields = [
-            'region', 'site', 'rack_group', 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'tags',
+            'region', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'tags',
         ]
         fieldsets = (
-            ('Reservation', ('region', 'site', 'rack_group', 'rack', 'units', 'user', 'description', 'tags')),
+            ('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
             ('Tenancy', ('tenant_group', 'tenant')),
         )
 
@@ -803,11 +805,11 @@ class RackReservationCSVForm(CustomFieldModelCSVForm):
         to_field_name='name',
         help_text='Parent site'
     )
-    rack_group = CSVModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = CSVModelChoiceField(
+        queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
-        help_text="Rack's group (if any)"
+        help_text="Rack's location (if any)"
     )
     rack = CSVModelChoiceField(
         queryset=Rack.objects.all(),
@@ -828,21 +830,21 @@ class RackReservationCSVForm(CustomFieldModelCSVForm):
 
     class Meta:
         model = RackReservation
-        fields = ('site', 'rack_group', 'rack', 'units', 'tenant', 'description')
+        fields = ('site', 'location', 'rack', 'units', 'tenant', 'description')
 
     def __init__(self, data=None, *args, **kwargs):
         super().__init__(data, *args, **kwargs)
 
         if data:
 
-            # Limit rack_group queryset by assigned site
+            # Limit location queryset by assigned site
             params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
-            self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params)
+            self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
 
             # Limit rack queryset by assigned site and group
             params = {
                 f"site__{self.fields['site'].to_field_name}": data.get('site'),
-                f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'),
+                f"location__{self.fields['location'].to_field_name}": data.get('location'),
             }
             self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
 
@@ -874,7 +876,7 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
 
 class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
     model = RackReservation
-    field_order = ['q', 'region_id', 'site_id', 'group_id', 'user_id', 'tenant_group_id', 'tenant_id']
+    field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id']
     q = forms.CharField(
         required=False,
         label=_('Search')
@@ -892,10 +894,10 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
         },
         label=_('Region')
     )
-    group_id = DynamicModelMultipleChoiceField(
-        queryset=RackGroup.objects.prefetch_related('site'),
+    location_id = DynamicModelMultipleChoiceField(
+        queryset=Location.objects.prefetch_related('site'),
         required=False,
-        label='Rack group',
+        label='Location',
         null_option='None'
     )
     user_id = DynamicModelMultipleChoiceField(
@@ -1782,8 +1784,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
             'region_id': '$region'
         }
     )
-    rack_group = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = DynamicModelChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         display_field='display_name',
         query_params={
@@ -1799,7 +1801,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         display_field='display_name',
         query_params={
             'site_id': '$site',
-            'group_id': '$rack_group',
+            'location_id': 'location',
         }
     )
     position = forms.IntegerField(
@@ -2003,11 +2005,11 @@ class DeviceCSVForm(BaseDeviceCSVForm):
         to_field_name='name',
         help_text='Assigned site'
     )
-    rack_group = CSVModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = CSVModelChoiceField(
+        queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
-        help_text="Rack's group (if any)"
+        help_text="Rack's location (if any)"
     )
     rack = CSVModelChoiceField(
         queryset=Rack.objects.all(),
@@ -2024,7 +2026,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
     class Meta(BaseDeviceCSVForm.Meta):
         fields = [
             'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
-            'site', 'rack_group', 'rack', 'position', 'face', 'cluster', 'comments',
+            'site', 'location', 'rack', 'position', 'face', 'cluster', 'comments',
         ]
 
     def __init__(self, data=None, *args, **kwargs):
@@ -2032,14 +2034,14 @@ class DeviceCSVForm(BaseDeviceCSVForm):
 
         if data:
 
-            # Limit rack_group queryset by assigned site
+            # Limit location queryset by assigned site
             params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
-            self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params)
+            self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
 
             # Limit rack queryset by assigned site and group
             params = {
                 f"site__{self.fields['site'].to_field_name}": data.get('site'),
-                f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'),
+                f"location__{self.fields['location'].to_field_name}": data.get('location'),
             }
             self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
 
@@ -2135,7 +2137,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
 class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
     model = Device
     field_order = [
-        'q', 'region_id', 'site_id', 'rack_group_id', 'rack_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
+        'q', 'region_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
         'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
     ]
     q = forms.CharField(
@@ -2153,10 +2155,10 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
             'region_id': '$region_id'
         }
     )
-    rack_group_id = DynamicModelMultipleChoiceField(
-        queryset=RackGroup.objects.all(),
+    location_id = DynamicModelMultipleChoiceField(
+        queryset=Location.objects.all(),
         required=False,
-        label=_('Rack group'),
+        label=_('Location'),
         query_params={
             'site_id': '$site_id'
         }
@@ -2167,7 +2169,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
         null_option='None',
         query_params={
             'site_id': '$site_id',
-            'group_id': '$rack_group_id',
+            'location_id': '$location_id',
         },
         label=_('Rack')
     )
@@ -3834,9 +3836,9 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
             'region_id': '$termination_b_region'
         }
     )
-    termination_b_rackgroup = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
-        label='Rack Group',
+    termination_b_location = DynamicModelChoiceField(
+        queryset=Location.objects.all(),
+        label='Location',
         required=False,
         display_field='cid',
         query_params={
@@ -3849,7 +3851,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
         required=False,
         query_params={
             'site_id': '$termination_b_site',
-            'rack_group_id': '$termination_b_rackgroup',
+            'location_id': '$termination_b_location',
         }
     )
     termination_b_id = DynamicModelChoiceField(
@@ -3868,7 +3870,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
         model = Cable
         fields = [
-            'termination_b_rackgroup', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
+            'termination_b_location', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
             'color', 'length', 'length_unit', 'tags',
         ]
 
@@ -4450,8 +4452,8 @@ class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
             'region_id': '$region'
         }
     )
-    rack_group = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = DynamicModelChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         query_params={
             'site_id': '$site'
@@ -4465,10 +4467,10 @@ class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
         model = PowerPanel
         fields = [
-            'region', 'site', 'rack_group', 'name', 'tags',
+            'region', 'site', 'location', 'name', 'tags',
         ]
         fieldsets = (
-            ('Power Panel', ('region', 'site', 'rack_group', 'name', 'tags')),
+            ('Power Panel', ('region', 'site', 'location', 'name', 'tags')),
         )
 
 
@@ -4478,8 +4480,8 @@ class PowerPanelCSVForm(CustomFieldModelCSVForm):
         to_field_name='name',
         help_text='Name of parent site'
     )
-    rack_group = CSVModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = CSVModelChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         to_field_name='name'
     )
@@ -4495,7 +4497,7 @@ class PowerPanelCSVForm(CustomFieldModelCSVForm):
 
             # Limit group queryset by assigned site
             params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
-            self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params)
+            self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
 
 
 class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
@@ -4517,8 +4519,8 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE
             'region_id': '$region'
         }
     )
-    rack_group = DynamicModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = DynamicModelChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         query_params={
             'site_id': '$site'
@@ -4526,7 +4528,7 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkE
     )
 
     class Meta:
-        nullable_fields = ['rack_group']
+        nullable_fields = ['location']
 
 
 class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
@@ -4548,14 +4550,14 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
         },
         label=_('Site')
     )
-    rack_group_id = DynamicModelMultipleChoiceField(
-        queryset=RackGroup.objects.all(),
+    location_id = DynamicModelMultipleChoiceField(
+        queryset=Location.objects.all(),
         required=False,
         null_option='None',
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Rack group')
+        label=_('Location')
     )
     tag = TagFilterField(model)
 
@@ -4632,11 +4634,11 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm):
         to_field_name='name',
         help_text='Upstream power panel'
     )
-    rack_group = CSVModelChoiceField(
-        queryset=RackGroup.objects.all(),
+    location = CSVModelChoiceField(
+        queryset=Location.objects.all(),
         to_field_name='name',
         required=False,
-        help_text="Rack's group (if any)"
+        help_text="Rack's location (if any)"
     )
     rack = CSVModelChoiceField(
         queryset=Rack.objects.all(),
@@ -4678,14 +4680,14 @@ class PowerFeedCSVForm(CustomFieldModelCSVForm):
             params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
             self.fields['power_panel'].queryset = self.fields['power_panel'].queryset.filter(**params)
 
-            # Limit rack_group queryset by site
+            # Limit location queryset by site
             params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
-            self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params)
+            self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
 
             # Limit rack queryset by site and group
             params = {
                 f"site__{self.fields['site'].to_field_name}": data.get('site'),
-                f"group__{self.fields['rack_group'].to_field_name}": data.get('rack_group'),
+                f"location__{self.fields['location'].to_field_name}": data.get('location'),
             }
             self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
 
@@ -4748,7 +4750,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
 
     class Meta:
         nullable_fields = [
-            'rackgroup', 'comments',
+            'location', 'comments',
         ]
 
 

+ 39 - 0
netbox/dcim/migrations/0126_rename_rackgroup_location.py

@@ -0,0 +1,39 @@
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0125_console_port_speed'),
+    ]
+
+    operations = [
+        migrations.RenameModel(
+            old_name='RackGroup',
+            new_name='Location',
+        ),
+        migrations.AlterModelOptions(
+            name='rack',
+            options={'ordering': ('site', 'location', '_name', 'pk')},
+        ),
+        migrations.AlterField(
+            model_name='location',
+            name='site',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='dcim.site'),
+        ),
+        migrations.RenameField(
+            model_name='powerpanel',
+            old_name='rack_group',
+            new_name='location',
+        ),
+        migrations.RenameField(
+            model_name='rack',
+            old_name='group',
+            new_name='location',
+        ),
+        migrations.AlterUniqueTogether(
+            name='rack',
+            unique_together={('location', 'facility_id'), ('location', 'name')},
+        ),
+    ]

+ 1 - 1
netbox/dcim/models/__init__.py

@@ -34,7 +34,7 @@ __all__ = (
     'PowerPort',
     'PowerPortTemplate',
     'Rack',
-    'RackGroup',
+    'Location',
     'RackReservation',
     'RackRole',
     'RearPort',

+ 2 - 2
netbox/dcim/models/devices.py

@@ -600,7 +600,7 @@ class Device(PrimaryModel, ConfigContextModel):
 
     csv_headers = [
         'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
-        'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
+        'site', 'location', 'rack_name', 'position', 'face', 'comments',
     ]
     clone_fields = [
         'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster',
@@ -799,7 +799,7 @@ class Device(PrimaryModel, ConfigContextModel):
             self.asset_tag,
             self.get_status_display(),
             self.site.name,
-            self.rack.group.name if self.rack and self.rack.group else None,
+            self.rack.location.name if self.rack and self.rack.location else None,
             self.rack.name if self.rack else None,
             self.position,
             self.get_face_display(),

+ 9 - 9
netbox/dcim/models/power.py

@@ -32,8 +32,8 @@ class PowerPanel(PrimaryModel):
         to='Site',
         on_delete=models.PROTECT
     )
-    rack_group = models.ForeignKey(
-        to='RackGroup',
+    location = models.ForeignKey(
+        to='dcim.Location',
         on_delete=models.PROTECT,
         blank=True,
         null=True
@@ -45,7 +45,7 @@ class PowerPanel(PrimaryModel):
 
     objects = RestrictedQuerySet.as_manager()
 
-    csv_headers = ['site', 'rack_group', 'name']
+    csv_headers = ['site', 'location', 'name']
 
     class Meta:
         ordering = ['site', 'name']
@@ -60,17 +60,17 @@ class PowerPanel(PrimaryModel):
     def to_csv(self):
         return (
             self.site.name,
-            self.rack_group.name if self.rack_group else None,
+            self.location.name if self.location else None,
             self.name,
         )
 
     def clean(self):
         super().clean()
 
-        # RackGroup must belong to assigned Site
-        if self.rack_group and self.rack_group.site != self.site:
+        # Location must belong to assigned Site
+        if self.location and self.location.site != self.site:
             raise ValidationError("Rack group {} ({}) is in a different site than {}".format(
-                self.rack_group, self.rack_group.site, self.site
+                self.location, self.location.site, self.site
             ))
 
 
@@ -138,7 +138,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
     objects = RestrictedQuerySet.as_manager()
 
     csv_headers = [
-        'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
+        'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
         'voltage', 'amperage', 'max_utilization', 'comments',
     ]
     clone_fields = [
@@ -160,7 +160,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
         return (
             self.power_panel.site.name,
             self.power_panel.name,
-            self.rack.group.name if self.rack and self.rack.group else None,
+            self.rack.location.name if self.rack and self.rack.location else None,
             self.rack.name if self.rack else None,
             self.name,
             self.get_status_display(),

+ 30 - 33
netbox/dcim/models/racks.py

@@ -16,21 +16,20 @@ from taggit.managers import TaggableManager
 from dcim.choices import *
 from dcim.constants import *
 from dcim.elevations import RackElevationSVG
-from extras.models import ObjectChange, TaggedItem
+from extras.models import TaggedItem
 from extras.utils import extras_features
 from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.querysets import RestrictedQuerySet
-from utilities.mptt import TreeManager
-from utilities.utils import array_to_string, serialize_object
+from utilities.utils import array_to_string
 from .device_components import PowerOutlet, PowerPort
 from .devices import Device
 from .power import PowerFeed
 
 __all__ = (
     'Rack',
-    'RackGroup',
+    'Location',
     'RackReservation',
     'RackRole',
 )
@@ -41,11 +40,10 @@ __all__ = (
 #
 
 @extras_features('custom_fields', 'export_templates', 'webhooks')
-class RackGroup(NestedGroupModel):
+class Location(NestedGroupModel):
     """
-    Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
-    example, if a Site spans a corporate campus, a RackGroup might be defined to represent each building within that
-    campus. If a Site instead represents a single building, a RackGroup might represent a single room or floor.
+    A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
+    site, or a room within a building, for example.
     """
     name = models.CharField(
         max_length=100
@@ -56,7 +54,7 @@ class RackGroup(NestedGroupModel):
     site = models.ForeignKey(
         to='dcim.Site',
         on_delete=models.CASCADE,
-        related_name='rack_groups'
+        related_name='locations'
     )
     parent = TreeForeignKey(
         to='self',
@@ -81,7 +79,7 @@ class RackGroup(NestedGroupModel):
         ]
 
     def get_absolute_url(self):
-        return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
+        return "{}?location_id={}".format(reverse('dcim:rack_list'), self.pk)
 
     def to_csv(self):
         return (
@@ -95,9 +93,9 @@ class RackGroup(NestedGroupModel):
     def clean(self):
         super().clean()
 
-        # Parent RackGroup (if any) must belong to the same Site
+        # Parent Location (if any) must belong to the same Site
         if self.parent and self.parent.site != self.site:
-            raise ValidationError(f"Parent rack group ({self.parent}) must belong to the same site ({self.site})")
+            raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})")
 
 
 @extras_features('custom_fields', 'export_templates', 'webhooks')
@@ -147,7 +145,7 @@ class RackRole(OrganizationalModel):
 class Rack(PrimaryModel):
     """
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
-    Each Rack is assigned to a Site and (optionally) a RackGroup.
+    Each Rack is assigned to a Site and (optionally) a Location.
     """
     name = models.CharField(
         max_length=100
@@ -169,13 +167,12 @@ class Rack(PrimaryModel):
         on_delete=models.PROTECT,
         related_name='racks'
     )
-    group = models.ForeignKey(
-        to='dcim.RackGroup',
+    location = models.ForeignKey(
+        to='dcim.Location',
         on_delete=models.SET_NULL,
         related_name='racks',
         blank=True,
-        null=True,
-        help_text='Assigned group'
+        null=True
     )
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
@@ -259,20 +256,20 @@ class Rack(PrimaryModel):
     objects = RestrictedQuerySet.as_manager()
 
     csv_headers = [
-        'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
+        'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
         'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
     ]
     clone_fields = [
-        'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
+        'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
         'outer_depth', 'outer_unit',
     ]
 
     class Meta:
-        ordering = ('site', 'group', '_name', 'pk')  # (site, group, name) may be non-unique
+        ordering = ('site', 'location', '_name', 'pk')  # (site, location, name) may be non-unique
         unique_together = (
-            # Name and facility_id must be unique *only* within a RackGroup
-            ('group', 'name'),
-            ('group', 'facility_id'),
+            # Name and facility_id must be unique *only* within a Location
+            ('location', 'name'),
+            ('location', 'facility_id'),
         )
 
     def __str__(self):
@@ -284,9 +281,9 @@ class Rack(PrimaryModel):
     def clean(self):
         super().clean()
 
-        # Validate group/site assignment
-        if self.site and self.group and self.group.site != self.site:
-            raise ValidationError(f"Assigned rack group must belong to parent site ({self.site}).")
+        # Validate location/site assignment
+        if self.site and self.location and self.location.site != self.site:
+            raise ValidationError(f"Assigned location must belong to parent site ({self.site}).")
 
         # Validate outer dimensions and unit
         if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
@@ -309,17 +306,17 @@ class Rack(PrimaryModel):
                             min_height
                         )
                     })
-            # Validate that Rack was assigned a group of its same site, if applicable
-            if self.group:
-                if self.group.site != self.site:
+            # Validate that Rack was assigned a Location of its same site, if applicable
+            if self.location:
+                if self.location.site != self.site:
                     raise ValidationError({
-                        'group': "Rack group must be from the same site, {}.".format(self.site)
+                        'location': f"Location must be from the same site, {self.site}."
                     })
 
     def to_csv(self):
         return (
             self.site.name,
-            self.group.name if self.group else None,
+            self.location.name if self.location else None,
             self.name,
             self.facility_id,
             self.tenant.name if self.tenant else None,
@@ -565,7 +562,7 @@ class RackReservation(PrimaryModel):
 
     objects = RestrictedQuerySet.as_manager()
 
-    csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
+    csv_headers = ['site', 'location', 'rack', 'units', 'tenant', 'user', 'description']
 
     class Meta:
         ordering = ['created', 'pk']
@@ -606,7 +603,7 @@ class RackReservation(PrimaryModel):
     def to_csv(self):
         return (
             self.rack.site.name,
-            self.rack.group if self.rack.group else None,
+            self.rack.location if self.rack.location else None,
             self.rack.name,
             ','.join([str(u) for u in self.units]),
             self.tenant.name if self.tenant else None,

+ 9 - 9
netbox/dcim/signals.py

@@ -7,7 +7,7 @@ from django.db import transaction
 from django.dispatch import receiver
 
 from .choices import CableStatusChoices
-from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, RackGroup, VirtualChassis
+from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
 
 
 def create_cablepath(node):
@@ -40,20 +40,20 @@ def rebuild_paths(obj):
 # Site/rack/device assignment
 #
 
-@receiver(post_save, sender=RackGroup)
-def handle_rackgroup_site_change(instance, created, **kwargs):
+@receiver(post_save, sender=Location)
+def handle_location_site_change(instance, created, **kwargs):
     """
-    Update child RackGroups and Racks if Site assignment has changed. We intentionally recurse through each child
+    Update child Locations and Racks if Site assignment has changed. We intentionally recurse through each child
     object instead of calling update() on the QuerySet to ensure the proper change records get created for each.
     """
     if not created:
-        for rackgroup in instance.get_children():
-            rackgroup.site = instance.site
-            rackgroup.save()
-        for rack in Rack.objects.filter(group=instance).exclude(site=instance.site):
+        for location in instance.get_children():
+            location.site = instance.site
+            location.save()
+        for rack in Rack.objects.filter(location=instance).exclude(site=instance.site):
             rack.site = instance.site
             rack.save()
-        for powerpanel in PowerPanel.objects.filter(rack_group=instance).exclude(site=instance.site):
+        for powerpanel in PowerPanel.objects.filter(location=instance).exclude(site=instance.site):
             powerpanel.site = instance.site
             powerpanel.save()
 

+ 2 - 2
netbox/dcim/tables/power.py

@@ -33,8 +33,8 @@ class PowerPanelTable(BaseTable):
 
     class Meta(BaseTable.Meta):
         model = PowerPanel
-        fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count', 'tags')
-        default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
+        fields = ('pk', 'name', 'site', 'location', 'powerfeed_count', 'tags')
+        default_columns = ('pk', 'name', 'site', 'location', 'powerfeed_count')
 
 
 #

+ 7 - 7
netbox/dcim/tables/racks.py

@@ -1,18 +1,18 @@
 import django_tables2 as tables
 from django_tables2.utils import Accessor
 
-from dcim.models import Rack, RackGroup, RackReservation, RackRole
+from dcim.models import Rack, Location, RackReservation, RackRole
 from tenancy.tables import COL_TENANT
 from utilities.tables import (
     BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn,
     ToggleColumn,
 )
-from .template_code import MPTT_LINK, RACKGROUP_ELEVATIONS, UTILIZATION_GRAPH
+from .template_code import MPTT_LINK, LOCATION_ELEVATIONS, UTILIZATION_GRAPH
 
 __all__ = (
     'RackTable',
     'RackDetailTable',
-    'RackGroupTable',
+    'LocationTable',
     'RackReservationTable',
     'RackRoleTable',
 )
@@ -22,7 +22,7 @@ __all__ = (
 # Rack groups
 #
 
-class RackGroupTable(BaseTable):
+class LocationTable(BaseTable):
     pk = ToggleColumn()
     name = tables.TemplateColumn(
         template_code=MPTT_LINK,
@@ -36,12 +36,12 @@ class RackGroupTable(BaseTable):
         verbose_name='Racks'
     )
     actions = ButtonsColumn(
-        model=RackGroup,
-        prepend_template=RACKGROUP_ELEVATIONS
+        model=Location,
+        prepend_template=LOCATION_ELEVATIONS
     )
 
     class Meta(BaseTable.Meta):
-        model = RackGroup
+        model = Location
         fields = ('pk', 'name', 'site', 'rack_count', 'description', 'slug', 'actions')
         default_columns = ('pk', 'name', 'site', 'rack_count', 'description', 'actions')
 

+ 2 - 2
netbox/dcim/tables/template_code.py

@@ -76,8 +76,8 @@ POWERFEED_CABLETERMINATION = """
 <a href="{{ value.get_absolute_url }}">{{ value }}</a>
 """
 
-RACKGROUP_ELEVATIONS = """
-<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
+LOCATION_ELEVATIONS = """
+<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&location_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
     <i class="mdi mdi-server"></i>
 </a>
 """

+ 46 - 46
netbox/dcim/tests/test_api.py

@@ -8,7 +8,7 @@ from dcim.models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
     InventoryItem, Platform, PowerFeed, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, PowerPanel,
-    Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
+    Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
 )
 from ipam.models import VLAN
 from utilities.testing import APITestCase, APIViewTestCases
@@ -135,8 +135,8 @@ class SiteTest(APIViewTestCases.APIViewTestCase):
         ]
 
 
-class RackGroupTest(APIViewTestCases.APIViewTestCase):
-    model = RackGroup
+class LocationTest(APIViewTestCases.APIViewTestCase):
+    model = Location
     brief_fields = ['_depth', 'id', 'name', 'rack_count', 'slug', 'url']
     bulk_update_data = {
         'description': 'New description',
@@ -151,33 +151,33 @@ class RackGroupTest(APIViewTestCases.APIViewTestCase):
         )
         Site.objects.bulk_create(sites)
 
-        parent_rack_groups = (
-            RackGroup.objects.create(site=sites[0], name='Parent Rack Group 1', slug='parent-rack-group-1'),
-            RackGroup.objects.create(site=sites[1], name='Parent Rack Group 2', slug='parent-rack-group-2'),
+        parent_locations = (
+            Location.objects.create(site=sites[0], name='Parent Location 1', slug='parent-location-1'),
+            Location.objects.create(site=sites[1], name='Parent Location 2', slug='parent-location-2'),
         )
 
-        RackGroup.objects.create(site=sites[0], name='Rack Group 1', slug='rack-group-1', parent=parent_rack_groups[0])
-        RackGroup.objects.create(site=sites[0], name='Rack Group 2', slug='rack-group-2', parent=parent_rack_groups[0])
-        RackGroup.objects.create(site=sites[0], name='Rack Group 3', slug='rack-group-3', parent=parent_rack_groups[0])
+        Location.objects.create(site=sites[0], name='Location 1', slug='location-1', parent=parent_locations[0])
+        Location.objects.create(site=sites[0], name='Location 2', slug='location-2', parent=parent_locations[0])
+        Location.objects.create(site=sites[0], name='Location 3', slug='location-3', parent=parent_locations[0])
 
         cls.create_data = [
             {
-                'name': 'Test Rack Group 4',
-                'slug': 'test-rack-group-4',
+                'name': 'Test Location 4',
+                'slug': 'test-location-4',
                 'site': sites[1].pk,
-                'parent': parent_rack_groups[1].pk,
+                'parent': parent_locations[1].pk,
             },
             {
-                'name': 'Test Rack Group 5',
-                'slug': 'test-rack-group-5',
+                'name': 'Test Location 5',
+                'slug': 'test-location-5',
                 'site': sites[1].pk,
-                'parent': parent_rack_groups[1].pk,
+                'parent': parent_locations[1].pk,
             },
             {
-                'name': 'Test Rack Group 6',
-                'slug': 'test-rack-group-6',
+                'name': 'Test Location 6',
+                'slug': 'test-location-6',
                 'site': sites[1].pk,
-                'parent': parent_rack_groups[1].pk,
+                'parent': parent_locations[1].pk,
             },
         ]
 
@@ -233,9 +233,9 @@ class RackTest(APIViewTestCases.APIViewTestCase):
         )
         Site.objects.bulk_create(sites)
 
-        rack_groups = (
-            RackGroup.objects.create(site=sites[0], name='Rack Group 1', slug='rack-group-1'),
-            RackGroup.objects.create(site=sites[1], name='Rack Group 2', slug='rack-group-2'),
+        locations = (
+            Location.objects.create(site=sites[0], name='Location 1', slug='location-1'),
+            Location.objects.create(site=sites[1], name='Location 2', slug='location-2'),
         )
 
         rack_roles = (
@@ -245,9 +245,9 @@ class RackTest(APIViewTestCases.APIViewTestCase):
         RackRole.objects.bulk_create(rack_roles)
 
         racks = (
-            Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 1'),
-            Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 2'),
-            Rack(site=sites[0], group=rack_groups[0], role=rack_roles[0], name='Rack 3'),
+            Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 1'),
+            Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 2'),
+            Rack(site=sites[0], location=locations[0], role=rack_roles[0], name='Rack 3'),
         )
         Rack.objects.bulk_create(racks)
 
@@ -255,19 +255,19 @@ class RackTest(APIViewTestCases.APIViewTestCase):
             {
                 'name': 'Test Rack 4',
                 'site': sites[1].pk,
-                'group': rack_groups[1].pk,
+                'location': locations[1].pk,
                 'role': rack_roles[1].pk,
             },
             {
                 'name': 'Test Rack 5',
                 'site': sites[1].pk,
-                'group': rack_groups[1].pk,
+                'location': locations[1].pk,
                 'role': rack_roles[1].pk,
             },
             {
                 'name': 'Test Rack 6',
                 'site': sites[1].pk,
-                'group': rack_groups[1].pk,
+                'location': locations[1].pk,
                 'role': rack_roles[1].pk,
             },
         ]
@@ -1588,17 +1588,17 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
             Site.objects.create(name='Site 2', slug='site-2'),
         )
 
-        rack_groups = (
-            RackGroup.objects.create(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
-            RackGroup.objects.create(name='Rack Group 2', slug='rack-group-2', site=sites[0]),
-            RackGroup.objects.create(name='Rack Group 3', slug='rack-group-3', site=sites[0]),
-            RackGroup.objects.create(name='Rack Group 4', slug='rack-group-3', site=sites[1]),
+        locations = (
+            Location.objects.create(name='Location 1', slug='location-1', site=sites[0]),
+            Location.objects.create(name='Location 2', slug='location-2', site=sites[0]),
+            Location.objects.create(name='Location 3', slug='location-3', site=sites[0]),
+            Location.objects.create(name='Location 4', slug='location-3', site=sites[1]),
         )
 
         power_panels = (
-            PowerPanel(site=sites[0], rack_group=rack_groups[0], name='Power Panel 1'),
-            PowerPanel(site=sites[0], rack_group=rack_groups[1], name='Power Panel 2'),
-            PowerPanel(site=sites[0], rack_group=rack_groups[2], name='Power Panel 3'),
+            PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
+            PowerPanel(site=sites[0], location=locations[1], name='Power Panel 2'),
+            PowerPanel(site=sites[0], location=locations[2], name='Power Panel 3'),
         )
         PowerPanel.objects.bulk_create(power_panels)
 
@@ -1606,23 +1606,23 @@ class PowerPanelTest(APIViewTestCases.APIViewTestCase):
             {
                 'name': 'Power Panel 4',
                 'site': sites[0].pk,
-                'rack_group': rack_groups[0].pk,
+                'location': locations[0].pk,
             },
             {
                 'name': 'Power Panel 5',
                 'site': sites[0].pk,
-                'rack_group': rack_groups[1].pk,
+                'location': locations[1].pk,
             },
             {
                 'name': 'Power Panel 6',
                 'site': sites[0].pk,
-                'rack_group': rack_groups[2].pk,
+                'location': locations[2].pk,
             },
         ]
 
         cls.bulk_update_data = {
             'site': sites[1].pk,
-            'rack_group': rack_groups[3].pk
+            'location': locations[3].pk
         }
 
 
@@ -1636,20 +1636,20 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
     @classmethod
     def setUpTestData(cls):
         site = Site.objects.create(name='Site 1', slug='site-1')
-        rackgroup = RackGroup.objects.create(site=site, name='Rack Group 1', slug='rack-group-1')
+        location = Location.objects.create(site=site, name='Location 1', slug='location-1')
         rackrole = RackRole.objects.create(name='Rack Role 1', slug='rack-role-1', color='ff0000')
 
         racks = (
-            Rack(site=site, group=rackgroup, role=rackrole, name='Rack 1'),
-            Rack(site=site, group=rackgroup, role=rackrole, name='Rack 2'),
-            Rack(site=site, group=rackgroup, role=rackrole, name='Rack 3'),
-            Rack(site=site, group=rackgroup, role=rackrole, name='Rack 4'),
+            Rack(site=site, location=location, role=rackrole, name='Rack 1'),
+            Rack(site=site, location=location, role=rackrole, name='Rack 2'),
+            Rack(site=site, location=location, role=rackrole, name='Rack 3'),
+            Rack(site=site, location=location, role=rackrole, name='Rack 4'),
         )
         Rack.objects.bulk_create(racks)
 
         power_panels = (
-            PowerPanel(site=site, rack_group=rackgroup, name='Power Panel 1'),
-            PowerPanel(site=site, rack_group=rackgroup, name='Power Panel 2'),
+            PowerPanel(site=site, location=location, name='Power Panel 1'),
+            PowerPanel(site=site, location=location, name='Power Panel 2'),
         )
         PowerPanel.objects.bulk_create(power_panels)
 

+ 69 - 69
netbox/dcim/tests/test_filters.py

@@ -7,7 +7,7 @@ from dcim.models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
     InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerPortTemplate, PowerOutlet,
-    PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
+    PowerOutletTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
     VirtualChassis,
 )
 from ipam.models import IPAddress
@@ -168,9 +168,9 @@ class SiteTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
-class RackGroupTestCase(TestCase):
-    queryset = RackGroup.objects.all()
-    filterset = RackGroupFilterSet
+class LocationTestCase(TestCase):
+    queryset = Location.objects.all()
+    filterset = LocationFilterSet
 
     @classmethod
     def setUpTestData(cls):
@@ -190,32 +190,32 @@ class RackGroupTestCase(TestCase):
         )
         Site.objects.bulk_create(sites)
 
-        parent_rack_groups = (
-            RackGroup(name='Parent Rack Group 1', slug='parent-rack-group-1', site=sites[0]),
-            RackGroup(name='Parent Rack Group 2', slug='parent-rack-group-2', site=sites[1]),
-            RackGroup(name='Parent Rack Group 3', slug='parent-rack-group-3', site=sites[2]),
+        parent_locations = (
+            Location(name='Parent Location 1', slug='parent-location-1', site=sites[0]),
+            Location(name='Parent Location 2', slug='parent-location-2', site=sites[1]),
+            Location(name='Parent Location 3', slug='parent-location-3', site=sites[2]),
         )
-        for rackgroup in parent_rack_groups:
-            rackgroup.save()
+        for location in parent_locations:
+            location.save()
 
-        rack_groups = (
-            RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0], parent=parent_rack_groups[0], description='A'),
-            RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1], parent=parent_rack_groups[1], description='B'),
-            RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2], parent=parent_rack_groups[2], description='C'),
+        locations = (
+            Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], description='A'),
+            Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], description='B'),
+            Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], description='C'),
         )
-        for rackgroup in rack_groups:
-            rackgroup.save()
+        for location in locations:
+            location.save()
 
     def test_id(self):
         params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_name(self):
-        params = {'name': ['Rack Group 1', 'Rack Group 2']}
+        params = {'name': ['Location 1', 'Location 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_slug(self):
-        params = {'slug': ['rack-group-1', 'rack-group-2']}
+        params = {'slug': ['location-1', 'location-2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_description(self):
@@ -237,7 +237,7 @@ class RackGroupTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
     def test_parent(self):
-        parent_groups = RackGroup.objects.filter(name__startswith='Parent')[:2]
+        parent_groups = Location.objects.filter(name__startswith='Parent')[:2]
         params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]}
@@ -297,13 +297,13 @@ class RackTestCase(TestCase):
         )
         Site.objects.bulk_create(sites)
 
-        rack_groups = (
-            RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
-            RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
-            RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
+        locations = (
+            Location(name='Location 1', slug='location-1', site=sites[0]),
+            Location(name='Location 2', slug='location-2', site=sites[1]),
+            Location(name='Location 3', slug='location-3', site=sites[2]),
         )
-        for rackgroup in rack_groups:
-            rackgroup.save()
+        for location in locations:
+            location.save()
 
         rack_roles = (
             RackRole(name='Rack Role 1', slug='rack-role-1'),
@@ -328,9 +328,9 @@ class RackTestCase(TestCase):
         Tenant.objects.bulk_create(tenants)
 
         racks = (
-            Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
-            Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
-            Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
+            Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
+            Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
+            Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
         )
         Rack.objects.bulk_create(racks)
 
@@ -395,11 +395,11 @@ class RackTestCase(TestCase):
         params = {'site': [sites[0].slug, sites[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
-    def test_group(self):
-        groups = RackGroup.objects.all()[:2]
-        params = {'group_id': [groups[0].pk, groups[1].pk]}
+    def test_location(self):
+        locations = Location.objects.all()[:2]
+        params = {'location_id': [locations[0].pk, locations[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-        params = {'group': [groups[0].slug, groups[1].slug]}
+        params = {'location': [locations[0].slug, locations[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_status(self):
@@ -448,18 +448,18 @@ class RackReservationTestCase(TestCase):
         )
         Site.objects.bulk_create(sites)
 
-        rack_groups = (
-            RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
-            RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
-            RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
+        locations = (
+            Location(name='Location 1', slug='location-1', site=sites[0]),
+            Location(name='Location 2', slug='location-2', site=sites[1]),
+            Location(name='Location 3', slug='location-3', site=sites[2]),
         )
-        for rackgroup in rack_groups:
-            rackgroup.save()
+        for location in locations:
+            location.save()
 
         racks = (
-            Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
-            Rack(name='Rack 2', site=sites[1], group=rack_groups[1]),
-            Rack(name='Rack 3', site=sites[2], group=rack_groups[2]),
+            Rack(name='Rack 1', site=sites[0], location=locations[0]),
+            Rack(name='Rack 2', site=sites[1], location=locations[1]),
+            Rack(name='Rack 3', site=sites[2], location=locations[2]),
         )
         Rack.objects.bulk_create(racks)
 
@@ -503,11 +503,11 @@ class RackReservationTestCase(TestCase):
         params = {'site': [sites[0].slug, sites[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
-    def test_group(self):
-        groups = RackGroup.objects.all()[:2]
-        params = {'group_id': [groups[0].pk, groups[1].pk]}
+    def test_location(self):
+        locations = Location.objects.all()[:2]
+        params = {'location_id': [locations[0].pk, locations[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-        params = {'group': [groups[0].slug, groups[1].slug]}
+        params = {'location': [locations[0].slug, locations[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_user(self):
@@ -1168,18 +1168,18 @@ class DeviceTestCase(TestCase):
         )
         Site.objects.bulk_create(sites)
 
-        rack_groups = (
-            RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
-            RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
-            RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
+        locations = (
+            Location(name='Location 1', slug='location-1', site=sites[0]),
+            Location(name='Location 2', slug='location-2', site=sites[1]),
+            Location(name='Location 3', slug='location-3', site=sites[2]),
         )
-        for rackgroup in rack_groups:
-            rackgroup.save()
+        for location in locations:
+            location.save()
 
         racks = (
-            Rack(name='Rack 1', site=sites[0], group=rack_groups[0]),
-            Rack(name='Rack 2', site=sites[1], group=rack_groups[1]),
-            Rack(name='Rack 3', site=sites[2], group=rack_groups[2]),
+            Rack(name='Rack 1', site=sites[0], location=locations[0]),
+            Rack(name='Rack 2', site=sites[1], location=locations[1]),
+            Rack(name='Rack 3', site=sites[2], location=locations[2]),
         )
         Rack.objects.bulk_create(racks)
 
@@ -1331,9 +1331,9 @@ class DeviceTestCase(TestCase):
         params = {'site': [sites[0].slug, sites[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
-    def test_rackgroup(self):
-        rack_groups = RackGroup.objects.all()[:2]
-        params = {'rack_group_id': [rack_groups[0].pk, rack_groups[1].pk]}
+    def test_location(self):
+        locations = Location.objects.all()[:2]
+        params = {'location_id': [locations[0].pk, locations[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_rack(self):
@@ -2589,18 +2589,18 @@ class PowerPanelTestCase(TestCase):
         )
         Site.objects.bulk_create(sites)
 
-        rack_groups = (
-            RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
-            RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
-            RackGroup(name='Rack Group 3', slug='rack-group-3', site=sites[2]),
+        locations = (
+            Location(name='Location 1', slug='location-1', site=sites[0]),
+            Location(name='Location 2', slug='location-2', site=sites[1]),
+            Location(name='Location 3', slug='location-3', site=sites[2]),
         )
-        for rackgroup in rack_groups:
-            rackgroup.save()
+        for location in locations:
+            location.save()
 
         power_panels = (
-            PowerPanel(name='Power Panel 1', site=sites[0], rack_group=rack_groups[0]),
-            PowerPanel(name='Power Panel 2', site=sites[1], rack_group=rack_groups[1]),
-            PowerPanel(name='Power Panel 3', site=sites[2], rack_group=rack_groups[2]),
+            PowerPanel(name='Power Panel 1', site=sites[0], location=locations[0]),
+            PowerPanel(name='Power Panel 2', site=sites[1], location=locations[1]),
+            PowerPanel(name='Power Panel 3', site=sites[2], location=locations[2]),
         )
         PowerPanel.objects.bulk_create(power_panels)
 
@@ -2626,9 +2626,9 @@ class PowerPanelTestCase(TestCase):
         params = {'site': [sites[0].slug, sites[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
-    def test_rack_group(self):
-        rack_groups = RackGroup.objects.all()[:2]
-        params = {'rack_group_id': [rack_groups[0].pk, rack_groups[1].pk]}
+    def test_location(self):
+        locations = Location.objects.all()[:2]
+        params = {'location_id': [locations[0].pk, locations[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 

+ 26 - 26
netbox/dcim/tests/test_models.py

@@ -7,37 +7,37 @@ from dcim.models import *
 from tenancy.models import Tenant
 
 
-class RackGroupTestCase(TestCase):
+class LocationTestCase(TestCase):
 
-    def test_change_rackgroup_site(self):
+    def test_change_location_site(self):
         """
-        Check that all child RackGroups and Racks get updated when a RackGroup is moved to a new Site. Topology:
+        Check that all child Locations and Racks get updated when a Location is moved to a new Site. Topology:
         Site A
-          - RackGroup A1
-            - RackGroup A2
+          - Location A1
+            - Location A2
               - Rack 2
             - Rack 1
         """
         site_a = Site.objects.create(name='Site A', slug='site-a')
         site_b = Site.objects.create(name='Site B', slug='site-b')
 
-        rackgroup_a1 = RackGroup(site=site_a, name='RackGroup A1', slug='rackgroup-a1')
-        rackgroup_a1.save()
-        rackgroup_a2 = RackGroup(site=site_a, parent=rackgroup_a1, name='RackGroup A2', slug='rackgroup-a2')
-        rackgroup_a2.save()
+        location_a1 = Location(site=site_a, name='Location A1', slug='location-a1')
+        location_a1.save()
+        location_a2 = Location(site=site_a, parent=location_a1, name='Location A2', slug='location-a2')
+        location_a2.save()
 
-        rack1 = Rack.objects.create(site=site_a, group=rackgroup_a1, name='Rack 1')
-        rack2 = Rack.objects.create(site=site_a, group=rackgroup_a2, name='Rack 2')
+        rack1 = Rack.objects.create(site=site_a, location=location_a1, name='Rack 1')
+        rack2 = Rack.objects.create(site=site_a, location=location_a2, name='Rack 2')
 
-        powerpanel1 = PowerPanel.objects.create(site=site_a, rack_group=rackgroup_a1, name='Power Panel 1')
+        powerpanel1 = PowerPanel.objects.create(site=site_a, location=location_a1, name='Power Panel 1')
 
-        # Move RackGroup A1 to Site B
-        rackgroup_a1.site = site_b
-        rackgroup_a1.save()
+        # Move Location A1 to Site B
+        location_a1.site = site_b
+        location_a1.save()
 
-        # Check that all objects within RackGroup A1 now belong to Site B
-        self.assertEqual(RackGroup.objects.get(pk=rackgroup_a1.pk).site, site_b)
-        self.assertEqual(RackGroup.objects.get(pk=rackgroup_a2.pk).site, site_b)
+        # Check that all objects within Location A1 now belong to Site B
+        self.assertEqual(Location.objects.get(pk=location_a1.pk).site, site_b)
+        self.assertEqual(Location.objects.get(pk=location_a2.pk).site, site_b)
         self.assertEqual(Rack.objects.get(pk=rack1.pk).site, site_b)
         self.assertEqual(Rack.objects.get(pk=rack2.pk).site, site_b)
         self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
@@ -55,12 +55,12 @@ class RackTestCase(TestCase):
             name='TestSite2',
             slug='test-site-2'
         )
-        self.group1 = RackGroup.objects.create(
+        self.location1 = Location.objects.create(
             name='TestGroup1',
             slug='test-group-1',
             site=self.site1
         )
-        self.group2 = RackGroup.objects.create(
+        self.location2 = Location.objects.create(
             name='TestGroup2',
             slug='test-group-2',
             site=self.site2
@@ -69,7 +69,7 @@ class RackTestCase(TestCase):
             name='TestRack1',
             facility_id='A101',
             site=self.site1,
-            group=self.group1,
+            location=self.location1,
             u_height=42
         )
         self.manufacturer = Manufacturer.objects.create(
@@ -134,19 +134,19 @@ class RackTestCase(TestCase):
         with self.assertRaises(ValidationError):
             rack1.clean()
 
-    def test_rack_group_site(self):
+    def test_location_site(self):
 
-        rack_invalid_group = Rack(
+        rack_invalid_location = Rack(
             name='TestRack2',
             facility_id='A102',
             site=self.site1,
             u_height=42,
-            group=self.group2
+            location=self.location2
         )
-        rack_invalid_group.save()
+        rack_invalid_location.save()
 
         with self.assertRaises(ValidationError):
-            rack_invalid_group.clean()
+            rack_invalid_location.clean()
 
     def test_mount_single_device(self):
 

+ 52 - 52
netbox/dcim/tests/test_views.py

@@ -117,8 +117,8 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
 
-class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
-    model = RackGroup
+class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
+    model = Location
 
     @classmethod
     def setUpTestData(cls):
@@ -126,26 +126,26 @@ class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
         site = Site(name='Site 1', slug='site-1')
         site.save()
 
-        rack_groups = (
-            RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
-            RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
-            RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
+        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),
         )
-        for rackgroup in rack_groups:
-            rackgroup.save()
+        for location in locations:
+            location.save()
 
         cls.form_data = {
-            'name': 'Rack Group X',
-            'slug': 'rack-group-x',
+            'name': 'Location X',
+            'slug': 'location-x',
             'site': site.pk,
-            'description': 'A new rack group',
+            'description': 'A new location',
         }
 
         cls.csv_data = (
             "site,name,slug,description",
-            "Site 1,Rack Group 4,rack-group-4,Fourth rack group",
-            "Site 1,Rack Group 5,rack-group-5,Fifth rack group",
-            "Site 1,Rack Group 6,rack-group-6,Sixth rack group",
+            "Site 1,Location 4,location-4,Fourth location",
+            "Site 1,Location 5,location-5,Fifth location",
+            "Site 1,Location 6,location-6,Sixth location",
         )
 
 
@@ -187,10 +187,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 
         site = Site.objects.create(name='Site 1', slug='site-1')
 
-        rack_group = RackGroup(name='Rack Group 1', slug='rack-group-1', site=site)
-        rack_group.save()
+        location = Location(name='Location 1', slug='location-1', site=site)
+        location.save()
 
-        rack = Rack(name='Rack 1', site=site, group=rack_group)
+        rack = Rack(name='Rack 1', site=site, location=location)
         rack.save()
 
         RackReservation.objects.bulk_create([
@@ -211,10 +211,10 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
         cls.csv_data = (
-            'site,rack_group,rack,units,description',
-            'Site 1,Rack Group 1,Rack 1,"10,11,12",Reservation 1',
-            'Site 1,Rack Group 1,Rack 1,"13,14,15",Reservation 2',
-            'Site 1,Rack Group 1,Rack 1,"16,17,18",Reservation 3',
+            'site,location,rack,units,description',
+            'Site 1,Location 1,Rack 1,"10,11,12",Reservation 1',
+            'Site 1,Location 1,Rack 1,"13,14,15",Reservation 2',
+            'Site 1,Location 1,Rack 1,"16,17,18",Reservation 3',
         )
 
         cls.bulk_edit_data = {
@@ -236,12 +236,12 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         )
         Site.objects.bulk_create(sites)
 
-        rackgroups = (
-            RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
-            RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1])
+        locations = (
+            Location(name='Location 1', slug='location-1', site=sites[0]),
+            Location(name='Location 2', slug='location-2', site=sites[1])
         )
-        for rackgroup in rackgroups:
-            rackgroup.save()
+        for location in locations:
+            location.save()
 
         rackroles = (
             RackRole(name='Rack Role 1', slug='rack-role-1'),
@@ -261,7 +261,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'name': 'Rack X',
             'facility_id': 'Facility X',
             'site': sites[1].pk,
-            'group': rackgroups[1].pk,
+            'location': locations[1].pk,
             'tenant': None,
             'status': RackStatusChoices.STATUS_PLANNED,
             'role': rackroles[1].pk,
@@ -279,15 +279,15 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
         cls.csv_data = (
-            "site,group,name,width,u_height",
+            "site,location,name,width,u_height",
             "Site 1,,Rack 4,19,42",
-            "Site 1,Rack Group 1,Rack 5,19,42",
-            "Site 2,Rack Group 2,Rack 6,19,42",
+            "Site 1,Location 1,Rack 5,19,42",
+            "Site 2,Location 2,Rack 6,19,42",
         )
 
         cls.bulk_edit_data = {
             'site': sites[1].pk,
-            'group': rackgroups[1].pk,
+            'location': locations[1].pk,
             'tenant': None,
             'status': RackStatusChoices.STATUS_DEPRECATED,
             'role': rackroles[1].pk,
@@ -929,11 +929,11 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         )
         Site.objects.bulk_create(sites)
 
-        rack_group = RackGroup(site=sites[0], name='Rack Group 1', slug='rack-group-1')
-        rack_group.save()
+        location = Location(site=sites[0], name='Location 1', slug='location-1')
+        location.save()
 
         racks = (
-            Rack(name='Rack 1', site=sites[0], group=rack_group),
+            Rack(name='Rack 1', site=sites[0], location=location),
             Rack(name='Rack 2', site=sites[1]),
         )
         Rack.objects.bulk_create(racks)
@@ -991,10 +991,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         }
 
         cls.csv_data = (
-            "device_role,manufacturer,device_type,status,name,site,rack_group,rack,position,face",
-            "Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Rack Group 1,Rack 1,10,front",
-            "Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Rack Group 1,Rack 1,20,front",
-            "Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Rack Group 1,Rack 1,30,front",
+            "device_role,manufacturer,device_type,status,name,site,location,rack,position,face",
+            "Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front",
+            "Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front",
+            "Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front",
         )
 
         cls.bulk_edit_data = {
@@ -1771,38 +1771,38 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         )
         Site.objects.bulk_create(sites)
 
-        rackgroups = (
-            RackGroup(name='Rack Group 1', slug='rack-group-1', site=sites[0]),
-            RackGroup(name='Rack Group 2', slug='rack-group-2', site=sites[1]),
+        locations = (
+            Location(name='Location 1', slug='location-1', site=sites[0]),
+            Location(name='Location 2', slug='location-2', site=sites[1]),
         )
-        for rackgroup in rackgroups:
-            rackgroup.save()
+        for location in locations:
+            location.save()
 
         PowerPanel.objects.bulk_create((
-            PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 1'),
-            PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 2'),
-            PowerPanel(site=sites[0], rack_group=rackgroups[0], name='Power Panel 3'),
+            PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
+            PowerPanel(site=sites[0], location=locations[0], name='Power Panel 2'),
+            PowerPanel(site=sites[0], location=locations[0], name='Power Panel 3'),
         ))
 
         tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'site': sites[1].pk,
-            'rack_group': rackgroups[1].pk,
+            'location': locations[1].pk,
             'name': 'Power Panel X',
             'tags': [t.pk for t in tags],
         }
 
         cls.csv_data = (
-            "site,rack_group,name",
-            "Site 1,Rack Group 1,Power Panel 4",
-            "Site 1,Rack Group 1,Power Panel 5",
-            "Site 1,Rack Group 1,Power Panel 6",
+            "site,location,name",
+            "Site 1,Location 1,Power Panel 4",
+            "Site 1,Location 1,Power Panel 5",
+            "Site 1,Location 1,Power Panel 6",
         )
 
         cls.bulk_edit_data = {
             'site': sites[1].pk,
-            'rack_group': rackgroups[1].pk,
+            'location': locations[1].pk,
         }
 
 

+ 9 - 9
netbox/dcim/urls.py

@@ -5,7 +5,7 @@ from ipam.views import ServiceEditView
 from . import views
 from .models import (
     Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, FrontPort, Interface,
-    InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup,
+    InventoryItem, Manufacturer, Platform, PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, Location,
     RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
 )
 
@@ -33,14 +33,14 @@ urlpatterns = [
     path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
     path('sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
 
-    # Rack groups
-    path('rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'),
-    path('rack-groups/add/', views.RackGroupEditView.as_view(), name='rackgroup_add'),
-    path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
-    path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
-    path('rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
-    path('rack-groups/<int:pk>/delete/', views.RackGroupDeleteView.as_view(), name='rackgroup_delete'),
-    path('rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
+    # Locations
+    path('locations/', views.LocationListView.as_view(), name='location_list'),
+    path('locations/add/', views.LocationEditView.as_view(), name='location_add'),
+    path('locations/import/', views.LocationBulkImportView.as_view(), name='location_import'),
+    path('locations/delete/', views.LocationBulkDeleteView.as_view(), name='location_bulk_delete'),
+    path('locations/<int:pk>/edit/', views.LocationEditView.as_view(), name='location_edit'),
+    path('locations/<int:pk>/delete/', views.LocationDeleteView.as_view(), name='location_delete'),
+    path('locations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='location_changelog', kwargs={'model': Location}),
 
     # Rack roles
     path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),

+ 39 - 39
netbox/dcim/views.py

@@ -30,7 +30,7 @@ from .models import (
     Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
     InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel,
-    PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
+    PowerPort, PowerPortTemplate, Rack, Location, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
     VirtualChassis,
 )
 
@@ -161,17 +161,17 @@ class SiteView(generic.ObjectView):
             'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).count(),
             'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance).count(),
         }
-        rack_groups = RackGroup.objects.add_related_count(
-            RackGroup.objects.all(),
+        locations = Location.objects.add_related_count(
+            Location.objects.all(),
             Rack,
-            'group',
+            'location',
             'rack_count',
             cumulative=True
         ).restrict(request.user, 'view').filter(site=instance)
 
         return {
             'stats': stats,
-            'rack_groups': rack_groups,
+            'locations': locations,
         }
 
 
@@ -207,44 +207,44 @@ class SiteBulkDeleteView(generic.BulkDeleteView):
 # Rack groups
 #
 
-class RackGroupListView(generic.ObjectListView):
-    queryset = RackGroup.objects.add_related_count(
-        RackGroup.objects.all(),
+class LocationListView(generic.ObjectListView):
+    queryset = Location.objects.add_related_count(
+        Location.objects.all(),
         Rack,
-        'group',
+        'location',
         'rack_count',
         cumulative=True
     )
-    filterset = filters.RackGroupFilterSet
-    filterset_form = forms.RackGroupFilterForm
-    table = tables.RackGroupTable
+    filterset = filters.LocationFilterSet
+    filterset_form = forms.LocationFilterForm
+    table = tables.LocationTable
 
 
-class RackGroupEditView(generic.ObjectEditView):
-    queryset = RackGroup.objects.all()
-    model_form = forms.RackGroupForm
+class LocationEditView(generic.ObjectEditView):
+    queryset = Location.objects.all()
+    model_form = forms.LocationForm
 
 
-class RackGroupDeleteView(generic.ObjectDeleteView):
-    queryset = RackGroup.objects.all()
+class LocationDeleteView(generic.ObjectDeleteView):
+    queryset = Location.objects.all()
 
 
-class RackGroupBulkImportView(generic.BulkImportView):
-    queryset = RackGroup.objects.all()
-    model_form = forms.RackGroupCSVForm
-    table = tables.RackGroupTable
+class LocationBulkImportView(generic.BulkImportView):
+    queryset = Location.objects.all()
+    model_form = forms.LocationCSVForm
+    table = tables.LocationTable
 
 
-class RackGroupBulkDeleteView(generic.BulkDeleteView):
-    queryset = RackGroup.objects.add_related_count(
-        RackGroup.objects.all(),
+class LocationBulkDeleteView(generic.BulkDeleteView):
+    queryset = Location.objects.add_related_count(
+        Location.objects.all(),
         Rack,
-        'group',
+        'location',
         'rack_count',
         cumulative=True
     ).prefetch_related('site')
-    filterset = filters.RackGroupFilterSet
-    table = tables.RackGroupTable
+    filterset = filters.LocationFilterSet
+    table = tables.LocationTable
 
 
 #
@@ -286,7 +286,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
 
 class RackListView(generic.ObjectListView):
     queryset = Rack.objects.prefetch_related(
-        'site', 'group', 'tenant', 'role', 'devices__device_type'
+        'site', 'location', 'tenant', 'role', 'devices__device_type'
     ).annotate(
         device_count=count_related(Device, 'rack')
     )
@@ -338,7 +338,7 @@ class RackElevationListView(generic.ObjectListView):
 
 
 class RackView(generic.ObjectView):
-    queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role')
+    queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
 
     def get_extra_context(self, request, instance):
         # Get 0U and child devices located within the rack
@@ -349,10 +349,10 @@ class RackView(generic.ObjectView):
 
         peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
 
-        if instance.group:
-            peer_racks = peer_racks.filter(group=instance.group)
+        if instance.location:
+            peer_racks = peer_racks.filter(location=instance.location)
         else:
-            peer_racks = peer_racks.filter(group__isnull=True)
+            peer_racks = peer_racks.filter(location__isnull=True)
         next_rack = peer_racks.filter(name__gt=instance.name).order_by('name').first()
         prev_rack = peer_racks.filter(name__lt=instance.name).order_by('-name').first()
 
@@ -390,14 +390,14 @@ class RackBulkImportView(generic.BulkImportView):
 
 
 class RackBulkEditView(generic.BulkEditView):
-    queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
+    queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
     filterset = filters.RackFilterSet
     table = tables.RackTable
     form = forms.RackBulkEditForm
 
 
 class RackBulkDeleteView(generic.BulkDeleteView):
-    queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
+    queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
     filterset = filters.RackFilterSet
     table = tables.RackTable
 
@@ -982,7 +982,7 @@ class DeviceListView(generic.ObjectListView):
 
 class DeviceView(generic.ObjectView):
     queryset = Device.objects.prefetch_related(
-        'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
+        'site__region', 'rack__location', 'tenant__group', 'device_role', 'platform', 'primary_ip4', 'primary_ip6'
     )
 
     def get_extra_context(self, request, instance):
@@ -2560,7 +2560,7 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
 
 class PowerPanelListView(generic.ObjectListView):
     queryset = PowerPanel.objects.prefetch_related(
-        'site', 'rack_group'
+        'site', 'location'
     ).annotate(
         powerfeed_count=count_related(PowerFeed, 'power_panel')
     )
@@ -2570,7 +2570,7 @@ class PowerPanelListView(generic.ObjectListView):
 
 
 class PowerPanelView(generic.ObjectView):
-    queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
+    queryset = PowerPanel.objects.prefetch_related('site', 'location')
 
     def get_extra_context(self, request, instance):
         power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance).prefetch_related('rack')
@@ -2601,7 +2601,7 @@ class PowerPanelBulkImportView(generic.BulkImportView):
 
 
 class PowerPanelBulkEditView(generic.BulkEditView):
-    queryset = PowerPanel.objects.prefetch_related('site', 'rack_group')
+    queryset = PowerPanel.objects.prefetch_related('site', 'location')
     filterset = filters.PowerPanelFilterSet
     table = tables.PowerPanelTable
     form = forms.PowerPanelBulkEditForm
@@ -2609,7 +2609,7 @@ class PowerPanelBulkEditView(generic.BulkEditView):
 
 class PowerPanelBulkDeleteView(generic.BulkDeleteView):
     queryset = PowerPanel.objects.prefetch_related(
-        'site', 'rack_group'
+        'site', 'location'
     ).annotate(
         powerfeed_count=count_related(PowerFeed, 'power_panel')
     )

+ 4 - 4
netbox/extras/tests/test_api.py

@@ -9,7 +9,7 @@ from django_rq.queues import get_connection
 from rest_framework import status
 from rq import Worker
 
-from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, RackGroup, RackRole, Site
+from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
 from extras.api.views import ReportViewSet, ScriptViewSet
 from extras.models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, Tag
 from extras.reports import Report
@@ -382,13 +382,13 @@ class CreatedUpdatedFilterTest(APITestCase):
         super().setUp()
 
         self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
-        self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1')
+        self.location1 = Location.objects.create(site=self.site1, name='Test Location 1', slug='test-location-1')
         self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
         self.rack1 = Rack.objects.create(
-            site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 1', u_height=42,
+            site=self.site1, location=self.location1, role=self.rackrole1, name='Test Rack 1', u_height=42,
         )
         self.rack2 = Rack.objects.create(
-            site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 2', u_height=42,
+            site=self.site1, location=self.location1, role=self.rackrole1, name='Test Rack 2', u_height=42,
         )
 
         # change the created and last_updated of one

+ 11 - 11
netbox/netbox/constants.py

@@ -6,12 +6,12 @@ from circuits.filters import CircuitFilterSet, ProviderFilterSet
 from circuits.models import Circuit, Provider
 from circuits.tables import CircuitTable, ProviderTable
 from dcim.filters import (
-    CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, RackGroupFilterSet,
+    CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet,
     SiteFilterSet, VirtualChassisFilterSet,
 )
-from dcim.models import Cable, Device, DeviceType, PowerFeed, Rack, RackGroup, Site, VirtualChassis
+from dcim.models import Cable, Device, DeviceType, PowerFeed, Rack, Location, Site, VirtualChassis
 from dcim.tables import (
-    CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
+    CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, LocationTable, SiteTable,
     VirtualChassisTable,
 )
 from ipam.filters import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
@@ -55,22 +55,22 @@ SEARCH_TYPES = OrderedDict((
         'url': 'dcim:site_list',
     }),
     ('rack', {
-        'queryset': Rack.objects.prefetch_related('site', 'group', 'tenant', 'role'),
+        'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role'),
         'filterset': RackFilterSet,
         'table': RackTable,
         'url': 'dcim:rack_list',
     }),
-    ('rackgroup', {
-        'queryset': RackGroup.objects.add_related_count(
-            RackGroup.objects.all(),
+    ('location', {
+        'queryset': Location.objects.add_related_count(
+            Location.objects.all(),
             Rack,
-            'group',
+            'location',
             'rack_count',
             cumulative=True
         ).prefetch_related('site'),
-        'filterset': RackGroupFilterSet,
-        'table': RackGroupTable,
-        'url': 'dcim:rackgroup_list',
+        'filterset': LocationFilterSet,
+        'table': LocationTable,
+        'url': 'dcim:location_list',
     }),
     ('devicetype', {
         'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(

+ 1 - 1
netbox/netbox/forms.py

@@ -11,7 +11,7 @@ OBJ_TYPE_CHOICES = (
     ('DCIM', (
         ('site', 'Sites'),
         ('rack', 'Racks'),
-        ('rackgroup', 'Rack Groups'),
+        ('location', 'Locations'),
         ('devicetype', 'Device types'),
         ('device', 'Devices'),
         ('virtualchassis', 'Virtual Chassis'),

+ 1 - 1
netbox/netbox/settings.py

@@ -428,7 +428,7 @@ CACHEOPS = {
     'circuits.*': {'ops': 'all'},
     'dcim.inventoryitem': None,  # MPTT models are exempt due to raw SQL
     'dcim.region': None,  # MPTT models are exempt due to raw SQL
-    'dcim.rackgroup': None,  # MPTT models are exempt due to raw SQL
+    'dcim.location': None,  # MPTT models are exempt due to raw SQL
     'dcim.*': {'ops': 'all'},
     'ipam.*': {'ops': 'all'},
     'extras.*': {'ops': 'all'},

+ 2 - 2
netbox/templates/dcim/cable_connect.html

@@ -123,8 +123,8 @@
                         {% if 'termination_b_site' in form.fields %}
                             {% render_field form.termination_b_site %}
                         {% endif %}
-                        {% if 'termination_b_rackgroup' in form.fields %}
-                            {% render_field form.termination_b_rackgroup %}
+                        {% if 'termination_b_location' in form.fields %}
+                            {% render_field form.termination_b_location %}
                         {% endif %}
                         {% if 'termination_b_rack' in form.fields %}
                             {% render_field form.termination_b_rack %}

+ 1 - 1
netbox/templates/dcim/device_edit.html

@@ -24,7 +24,7 @@
         <div class="panel-body">
             {% render_field form.region %}
             {% render_field form.site %}
-            {% render_field form.rack_group %}
+            {% render_field form.location %}
             {% render_field form.rack %}
             {% if obj.device_type.is_child_device and obj.parent_bay %}
                 <div class="form-group">

+ 5 - 5
netbox/templates/dcim/powerpanel.html

@@ -5,8 +5,8 @@
 {% block breadcrumbs %}
   <li><a href="{% url 'dcim:powerpanel_list' %}">Power Panels</a></li>
   <li><a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a></li>
-  {% if object.rack_group %}
-    <li><a href="{{ object.rack_group.get_absolute_url }}">{{ object.rack_group }}</a></li>
+  {% if object.location %}
+    <li><a href="{{ object.location.get_absolute_url }}">{{ object.location }}</a></li>
   {% endif %}
   <li>{{ object }}</li>
 {% endblock %}
@@ -26,10 +26,10 @@
                     </td>
                 </tr>
                 <tr>
-                    <td>Rack Group</td>
+                    <td>Location</td>
                     <td>
-                        {% if object.rack_group %}
-                            <a href="{{ object.rack_group.get_absolute_url }}">{{ object.rack_group }}</a>
+                        {% if object.location %}
+                            <a href="{{ object.location.get_absolute_url }}">{{ object.location }}</a>
                         {% else %}
                             <span class="text-muted">None</span>
                         {% endif %}

+ 5 - 5
netbox/templates/dcim/rack.html

@@ -61,13 +61,13 @@
                     </td>
                 </tr>
                 <tr>
-                    <td>Group</td>
+                    <td>Location</td>
                     <td>
-                        {% if object.group %}
-                            {% for group in object.group.get_ancestors %}
-                                <a href="{{ group.get_absolute_url }}">{{ group }}</a> /
+                        {% if object.location %}
+                            {% for location in object.location.get_ancestors %}
+                                <a href="{{ location.get_absolute_url }}">{{ location }}</a> /
                             {% endfor %}
-                            <a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
+                            <a href="{{ object.location.get_absolute_url }}">{{ object.location }}</a>
                         {% else %}
                             <span class="text-muted">None</span>
                         {% endif %}

+ 1 - 1
netbox/templates/dcim/rack_edit.html

@@ -7,7 +7,7 @@
         <div class="panel-body">
             {% render_field form.region %}
             {% render_field form.site %}
-            {% render_field form.group %}
+            {% render_field form.location %}
             {% render_field form.name %}
             {% render_field form.status %}
             {% render_field form.role %}

+ 7 - 5
netbox/templates/dcim/site.html

@@ -194,15 +194,17 @@
         </div>
         <div class="panel panel-default">
             <div class="panel-heading">
-                <strong>Rack Groups</strong>
+                <strong>Locations</strong>
             </div>
             <table class="table table-hover panel-body">
-                {% for rg in rack_groups %}
+                {% for location in locations %}
                     <tr>
-                        <td style="padding-left: {{ rg.level }}8px"><i class="mdi mdi-folder-open"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
-                        <td>{{ rg.rack_count }}</td>
+                        <td style="padding-left: {{ location.level }}8px">
+                          <i class="mdi mdi-folder-open"></i> <a href="{{ location.get_absolute_url }}">{{ location }}</a>
+                        </td>
+                        <td>{{ location.rack_count }}</td>
                         <td class="text-right noprint">
-                            <a href="{% url 'dcim:rack_elevation_list' %}?group_id={{ rg.pk }}" class="btn btn-xs btn-primary" title="View elevations">
+                            <a href="{% url 'dcim:rack_elevation_list' %}?location_id={{ location.pk }}" class="btn btn-xs btn-primary" title="View elevations">
                                 <i class="mdi mdi-server"></i>
                             </a>
                         </td>

+ 5 - 5
netbox/templates/inc/nav_menu.html

@@ -49,14 +49,14 @@
                             {% endif %}
                             <a href="{% url 'dcim:rack_list' %}">Racks</a>
                         </li>
-                        <li{% if not perms.dcim.view_rackgroup %} class="disabled"{% endif %}>
-                            {% if perms.dcim.add_rackgroup %}
+                        <li{% if not perms.dcim.view_location %} class="disabled"{% endif %}>
+                            {% if perms.dcim.add_location %}
                                 <div class="buttons pull-right">
-                                    <a href="{% url 'dcim:rackgroup_add' %}" class="btn btn-xs btn-success" title="Add"><i class="mdi mdi-plus-thick"></i></a>
-                                    <a href="{% url 'dcim:rackgroup_import' %}" class="btn btn-xs btn-info" title="Import"><i class="mdi mdi-database-import-outline"></i></a>
+                                    <a href="{% url 'dcim:location_add' %}" class="btn btn-xs btn-success" title="Add"><i class="mdi mdi-plus-thick"></i></a>
+                                    <a href="{% url 'dcim:location_import' %}" class="btn btn-xs btn-info" title="Import"><i class="mdi mdi-database-import-outline"></i></a>
                                 </div>
                             {% endif %}
-                            <a href="{% url 'dcim:rackgroup_list' %}">Rack Groups</a>
+                            <a href="{% url 'dcim:location_list' %}">Locations</a>
                         </li>
                         <li{% if not perms.dcim.view_rackrole %} class="disabled"{% endif %}>
                             {% if perms.dcim.add_rackrole %}