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

Closes #9582: Enable assigning config contexts based on device location

jeremystretch 3 лет назад
Родитель
Сommit
379880cd84

+ 2 - 0
docs/models/extras/configcontext.md

@@ -5,9 +5,11 @@ Sometimes it is desirable to associate additional data with a group of devices o
 * Region
 * Region
 * Site group
 * Site group
 * Site
 * Site
+* Location (devices only)
 * Device type (devices only)
 * Device type (devices only)
 * Role
 * Role
 * Platform
 * Platform
+* Cluster type (VMs only)
 * Cluster group (VMs only)
 * Cluster group (VMs only)
 * Cluster (VMs only)
 * Cluster (VMs only)
 * Tenant group
 * Tenant group

+ 3 - 0
docs/release-notes/version-3.3.md

@@ -25,6 +25,7 @@
 * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
 * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
 * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
 * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
 * [#9166](https://github.com/netbox-community/netbox/issues/9166) - Add UI visibility toggle for custom fields
 * [#9166](https://github.com/netbox-community/netbox/issues/9166) - Add UI visibility toggle for custom fields
+* [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location
 
 
 ### Other Changes
 ### Other Changes
 
 
@@ -45,6 +46,8 @@
     * Added required `status` field (default value: `active`)
     * Added required `status` field (default value: `active`)
 * dcim.Rack
 * dcim.Rack
     * The `elevation` endpoint now includes half-height rack units, and utilizes decimal values for the ID and name of each unit
     * The `elevation` endpoint now includes half-height rack units, and utilizes decimal values for the ID and name of each unit
+* extras.ConfigContext
+    * Added the `locations` many-to-many field to track the assignment of ConfigContexts to Locations
 * extras.CustomField
 * extras.CustomField
     * Added `group_name` and `ui_visibility` fields
     * Added `group_name` and `ui_visibility` fields
 * ipam.IPAddress
 * ipam.IPAddress

+ 11 - 5
netbox/extras/api/serializers.py

@@ -5,10 +5,10 @@ from drf_yasg.utils import swagger_serializer_method
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from dcim.api.nested_serializers import (
 from dcim.api.nested_serializers import (
-    NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedRegionSerializer,
-    NestedSiteSerializer, NestedSiteGroupSerializer,
+    NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
+    NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
 )
 )
-from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 from extras.choices import *
 from extras.choices import *
 from extras.models import *
 from extras.models import *
 from extras.utils import FeatureQuery
 from extras.utils import FeatureQuery
@@ -272,6 +272,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         required=False,
         required=False,
         many=True
         many=True
     )
     )
+    locations = SerializedPKRelatedField(
+        queryset=Location.objects.all(),
+        serializer=NestedLocationSerializer,
+        required=False,
+        many=True
+    )
     device_types = SerializedPKRelatedField(
     device_types = SerializedPKRelatedField(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
         serializer=NestedDeviceTypeSerializer,
         serializer=NestedDeviceTypeSerializer,
@@ -331,8 +337,8 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         model = ConfigContext
         model = ConfigContext
         fields = [
         fields = [
             'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
             'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
-            'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
-            'tenants', 'tags', 'data', 'created', 'last_updated',
+            'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
+            'tenant_groups', 'tenants', 'tags', 'data', 'created', 'last_updated',
         ]
         ]
 
 
 
 

+ 1 - 1
netbox/extras/api/views.py

@@ -138,7 +138,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):
 
 
 class ConfigContextViewSet(NetBoxModelViewSet):
 class ConfigContextViewSet(NetBoxModelViewSet):
     queryset = ConfigContext.objects.prefetch_related(
     queryset = ConfigContext.objects.prefetch_related(
-        'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
+        'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants',
     )
     )
     serializer_class = serializers.ConfigContextSerializer
     serializer_class = serializers.ConfigContextSerializer
     filterset_class = filtersets.ConfigContextFilterSet
     filterset_class = filtersets.ConfigContextFilterSet

+ 12 - 1
netbox/extras/filtersets.py

@@ -3,7 +3,7 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
 from django.db.models import Q
 
 
-from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
 from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
 from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
@@ -255,6 +255,17 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Site (slug)',
         label='Site (slug)',
     )
     )
+    location_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='locations',
+        queryset=Location.objects.all(),
+        label='Location',
+    )
+    location = django_filters.ModelMultipleChoiceFilter(
+        field_name='locations__slug',
+        queryset=Location.objects.all(),
+        to_field_name='slug',
+        label='Location (slug)',
+    )
     device_type_id = django_filters.ModelMultipleChoiceFilter(
     device_type_id = django_filters.ModelMultipleChoiceFilter(
         field_name='device_types',
         field_name='device_types',
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),

+ 7 - 2
netbox/extras/forms/filtersets.py

@@ -3,7 +3,7 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
-from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 from extras.choices import *
 from extras.choices import *
 from extras.models import *
 from extras.models import *
 from extras.utils import FeatureQuery
 from extras.utils import FeatureQuery
@@ -170,7 +170,7 @@ class TagFilterForm(FilterForm):
 class ConfigContextFilterForm(FilterForm):
 class ConfigContextFilterForm(FilterForm):
     fieldsets = (
     fieldsets = (
         (None, ('q', 'tag_id')),
         (None, ('q', 'tag_id')),
-        ('Location', ('region_id', 'site_group_id', 'site_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
         ('Device', ('device_type_id', 'platform_id', 'role_id')),
         ('Device', ('device_type_id', 'platform_id', 'role_id')),
         ('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
         ('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
         ('Tenant', ('tenant_group_id', 'tenant_id'))
         ('Tenant', ('tenant_group_id', 'tenant_id'))
@@ -190,6 +190,11 @@ class ConfigContextFilterForm(FilterForm):
         required=False,
         required=False,
         label=_('Sites')
         label=_('Sites')
     )
     )
+    location_id = DynamicModelMultipleChoiceField(
+        queryset=Location.objects.all(),
+        required=False,
+        label=_('Locations')
+    )
     device_type_id = DynamicModelMultipleChoiceField(
     device_type_id = DynamicModelMultipleChoiceField(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
         required=False,
         required=False,

+ 16 - 5
netbox/extras/forms/models.py

@@ -1,7 +1,7 @@
 from django import forms
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 
 
-from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
 from extras.choices import *
 from extras.choices import *
 from extras.models import *
 from extras.models import *
 from extras.utils import FeatureQuery
 from extras.utils import FeatureQuery
@@ -166,6 +166,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         required=False
         required=False
     )
     )
+    locations = DynamicModelMultipleChoiceField(
+        queryset=Location.objects.all(),
+        required=False
+    )
     device_types = DynamicModelMultipleChoiceField(
     device_types = DynamicModelMultipleChoiceField(
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
         required=False
         required=False
@@ -202,15 +206,22 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
         queryset=Tag.objects.all(),
         queryset=Tag.objects.all(),
         required=False
         required=False
     )
     )
-    data = JSONField(
-        label=''
+    data = JSONField()
+
+    fieldsets = (
+        ('Config Context', ('name', 'weight', 'description', 'data', 'is_active')),
+        ('Assignment', (
+            'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
+            'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
+        )),
     )
     )
 
 
     class Meta:
     class Meta:
         model = ConfigContext
         model = ConfigContext
         fields = (
         fields = (
-            'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
-            'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
+            'name', 'weight', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations',
+            'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
+            'tenants', 'tags',
         )
         )
 
 
 
 

+ 19 - 0
netbox/extras/migrations/0076_configcontext_locations.py

@@ -0,0 +1,19 @@
+# Generated by Django 4.0.5 on 2022-06-22 19:13
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0156_location_status'),
+        ('extras', '0075_customfield_ui_visibility'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='configcontext',
+            name='locations',
+            field=models.ManyToManyField(blank=True, related_name='+', to='dcim.location'),
+        ),
+    ]

+ 7 - 5
netbox/extras/models/configcontexts.py

@@ -1,5 +1,3 @@
-from collections import OrderedDict
-
 from django.core.validators import ValidationError
 from django.core.validators import ValidationError
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
@@ -55,6 +53,11 @@ class ConfigContext(WebhooksMixin, ChangeLoggedModel):
         related_name='+',
         related_name='+',
         blank=True
         blank=True
     )
     )
+    locations = models.ManyToManyField(
+        to='dcim.Location',
+        related_name='+',
+        blank=True
+    )
     device_types = models.ManyToManyField(
     device_types = models.ManyToManyField(
         to='dcim.DeviceType',
         to='dcim.DeviceType',
         related_name='+',
         related_name='+',
@@ -138,11 +141,10 @@ class ConfigContextModel(models.Model):
 
 
     def get_config_context(self):
     def get_config_context(self):
         """
         """
+        Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs.
         Return the rendered configuration context for a device or VM.
         Return the rendered configuration context for a device or VM.
         """
         """
-
-        # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
-        data = OrderedDict()
+        data = {}
 
 
         if not hasattr(self, 'config_context_data'):
         if not hasattr(self, 'config_context_data'):
             # The annotation is not available, so we fall back to manually querying for the config context objects
             # The annotation is not available, so we fall back to manually querying for the config context objects

+ 4 - 1
netbox/extras/querysets.py

@@ -19,8 +19,9 @@ class ConfigContextQuerySet(RestrictedQuerySet):
         # `device_role` for Device; `role` for VirtualMachine
         # `device_role` for Device; `role` for VirtualMachine
         role = getattr(obj, 'device_role', None) or obj.role
         role = getattr(obj, 'device_role', None) or obj.role
 
 
-        # Device type assignment is relevant only for Devices
+        # Device type and location assignment is relevant only for Devices
         device_type = getattr(obj, 'device_type', None)
         device_type = getattr(obj, 'device_type', None)
+        location = getattr(obj, 'location', None)
 
 
         # Get assigned cluster, group, and type (if any)
         # Get assigned cluster, group, and type (if any)
         cluster = getattr(obj, 'cluster', None)
         cluster = getattr(obj, 'cluster', None)
@@ -42,6 +43,7 @@ class ConfigContextQuerySet(RestrictedQuerySet):
             Q(regions__in=regions) | Q(regions=None),
             Q(regions__in=regions) | Q(regions=None),
             Q(site_groups__in=sitegroups) | Q(site_groups=None),
             Q(site_groups__in=sitegroups) | Q(site_groups=None),
             Q(sites=obj.site) | Q(sites=None),
             Q(sites=obj.site) | Q(sites=None),
+            Q(locations=location) | Q(locations=None),
             Q(device_types=device_type) | Q(device_types=None),
             Q(device_types=device_type) | Q(device_types=None),
             Q(roles=role) | Q(roles=None),
             Q(roles=role) | Q(roles=None),
             Q(platforms=obj.platform) | Q(platforms=None),
             Q(platforms=obj.platform) | Q(platforms=None),
@@ -114,6 +116,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
         )
         )
 
 
         if self.model._meta.model_name == 'device':
         if self.model._meta.model_name == 'device':
+            base_query.add((Q(locations=OuterRef('location')) | Q(locations=None)), Q.AND)
             base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
             base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
             base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
             base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
             base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)
             base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)

+ 3 - 2
netbox/extras/tables/tables.py

@@ -167,8 +167,9 @@ class ConfigContextTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = ConfigContext
         model = ConfigContext
         fields = (
         fields = (
-            'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', 'platforms',
-            'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created', 'last_updated',
+            'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'locations', 'roles',
+            'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created',
+            'last_updated',
         )
         )
         default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
         default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
 
 

+ 23 - 7
netbox/extras/tests/test_filtersets.py

@@ -6,7 +6,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.test import TestCase
 
 
 from circuits.models import Provider
 from circuits.models import Provider
-from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Location, Manufacturer, Platform, Rack, Region, Site, SiteGroup
 from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
 from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
 from extras.filtersets import *
 from extras.filtersets import *
 from extras.models import *
 from extras.models import *
@@ -368,9 +368,9 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
         regions = (
         regions = (
-            Region(name='Test Region 1', slug='test-region-1'),
-            Region(name='Test Region 2', slug='test-region-2'),
-            Region(name='Test Region 3', slug='test-region-3'),
+            Region(name='Region 1', slug='region-1'),
+            Region(name='Region 2', slug='region-2'),
+            Region(name='Region 3', slug='region-3'),
         )
         )
         for r in regions:
         for r in regions:
             r.save()
             r.save()
@@ -384,12 +384,20 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
             site_group.save()
             site_group.save()
 
 
         sites = (
         sites = (
-            Site(name='Test Site 1', slug='test-site-1'),
-            Site(name='Test Site 2', slug='test-site-2'),
-            Site(name='Test Site 3', slug='test-site-3'),
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+            Site(name='Site 3', slug='site-3'),
         )
         )
         Site.objects.bulk_create(sites)
         Site.objects.bulk_create(sites)
 
 
+        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 location in locations:
+            location.save()
+
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         device_types = (
         device_types = (
             DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
             DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
@@ -460,6 +468,7 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
             c.regions.set([regions[i]])
             c.regions.set([regions[i]])
             c.site_groups.set([site_groups[i]])
             c.site_groups.set([site_groups[i]])
             c.sites.set([sites[i]])
             c.sites.set([sites[i]])
+            c.locations.set([locations[i]])
             c.device_types.set([device_types[i]])
             c.device_types.set([device_types[i]])
             c.roles.set([device_roles[i]])
             c.roles.set([device_roles[i]])
             c.platforms.set([platforms[i]])
             c.platforms.set([platforms[i]])
@@ -501,6 +510,13 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'site': [sites[0].slug, sites[1].slug]}
         params = {'site': [sites[0].slug, sites[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
+    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 = {'location': [locations[0].slug, locations[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_device_type(self):
     def test_device_type(self):
         device_types = DeviceType.objects.all()[:2]
         device_types = DeviceType.objects.all()[:2]
         params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
         params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}

+ 29 - 19
netbox/extras/tests/test_models.py

@@ -1,6 +1,6 @@
 from django.test import TestCase
 from django.test import TestCase
 
 
-from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site, SiteGroup
+from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
 from extras.models import ConfigContext, Tag
 from extras.models import ConfigContext, Tag
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@@ -29,7 +29,8 @@ class ConfigContextTest(TestCase):
         self.devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
         self.devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
         self.region = Region.objects.create(name="Region")
         self.region = Region.objects.create(name="Region")
         self.sitegroup = SiteGroup.objects.create(name="Site Group")
         self.sitegroup = SiteGroup.objects.create(name="Site Group")
-        self.site = Site.objects.create(name='Site-1', slug='site-1', region=self.region, group=self.sitegroup)
+        self.site = Site.objects.create(name='Site 1', slug='site-1', region=self.region, group=self.sitegroup)
+        self.location = Location.objects.create(name='Location 1', slug='location-1', site=self.site)
         self.platform = Platform.objects.create(name="Platform")
         self.platform = Platform.objects.create(name="Platform")
         self.tenantgroup = TenantGroup.objects.create(name="Tenant Group")
         self.tenantgroup = TenantGroup.objects.create(name="Tenant Group")
         self.tenant = Tenant.objects.create(name="Tenant", group=self.tenantgroup)
         self.tenant = Tenant.objects.create(name="Tenant", group=self.tenantgroup)
@@ -40,7 +41,8 @@ class ConfigContextTest(TestCase):
             name='Device 1',
             name='Device 1',
             device_type=self.devicetype,
             device_type=self.devicetype,
             device_role=self.devicerole,
             device_role=self.devicerole,
-            site=self.site
+            site=self.site,
+            location=self.location
         )
         )
 
 
     def test_higher_weight_wins(self):
     def test_higher_weight_wins(self):
@@ -144,15 +146,6 @@ class ConfigContextTest(TestCase):
         self.assertEqual(self.device.get_config_context(), annotated_queryset[0].get_config_context())
         self.assertEqual(self.device.get_config_context(), annotated_queryset[0].get_config_context())
 
 
     def test_annotation_same_as_get_for_object_device_relations(self):
     def test_annotation_same_as_get_for_object_device_relations(self):
-
-        site_context = ConfigContext.objects.create(
-            name="site",
-            weight=100,
-            data={
-                "site": 1
-            }
-        )
-        site_context.sites.add(self.site)
         region_context = ConfigContext.objects.create(
         region_context = ConfigContext.objects.create(
             name="region",
             name="region",
             weight=100,
             weight=100,
@@ -169,6 +162,22 @@ class ConfigContextTest(TestCase):
             }
             }
         )
         )
         sitegroup_context.site_groups.add(self.sitegroup)
         sitegroup_context.site_groups.add(self.sitegroup)
+        site_context = ConfigContext.objects.create(
+            name="site",
+            weight=100,
+            data={
+                "site": 1
+            }
+        )
+        site_context.sites.add(self.site)
+        location_context = ConfigContext.objects.create(
+            name="location",
+            weight=100,
+            data={
+                "location": 1
+            }
+        )
+        location_context.locations.add(self.location)
         platform_context = ConfigContext.objects.create(
         platform_context = ConfigContext.objects.create(
             name="platform",
             name="platform",
             weight=100,
             weight=100,
@@ -205,6 +214,7 @@ class ConfigContextTest(TestCase):
         device = Device.objects.create(
         device = Device.objects.create(
             name="Device 2",
             name="Device 2",
             site=self.site,
             site=self.site,
+            location=self.location,
             tenant=self.tenant,
             tenant=self.tenant,
             platform=self.platform,
             platform=self.platform,
             device_role=self.devicerole,
             device_role=self.devicerole,
@@ -220,13 +230,6 @@ class ConfigContextTest(TestCase):
         cluster_group = ClusterGroup.objects.create(name="Cluster Group")
         cluster_group = ClusterGroup.objects.create(name="Cluster Group")
         cluster = Cluster.objects.create(name="Cluster", group=cluster_group, type=cluster_type)
         cluster = Cluster.objects.create(name="Cluster", group=cluster_group, type=cluster_type)
 
 
-        site_context = ConfigContext.objects.create(
-            name="site",
-            weight=100,
-            data={"site": 1}
-        )
-        site_context.sites.add(self.site)
-
         region_context = ConfigContext.objects.create(
         region_context = ConfigContext.objects.create(
             name="region",
             name="region",
             weight=100,
             weight=100,
@@ -241,6 +244,13 @@ class ConfigContextTest(TestCase):
         )
         )
         sitegroup_context.site_groups.add(self.sitegroup)
         sitegroup_context.site_groups.add(self.sitegroup)
 
 
+        site_context = ConfigContext.objects.create(
+            name="site",
+            weight=100,
+            data={"site": 1}
+        )
+        site_context.sites.add(self.site)
+
         platform_context = ConfigContext.objects.create(
         platform_context = ConfigContext.objects.create(
             name="platform",
             name="platform",
             weight=100,
             weight=100,

+ 1 - 1
netbox/extras/views.py

@@ -281,6 +281,7 @@ class ConfigContextView(generic.ObjectView):
             ('Regions', instance.regions.all),
             ('Regions', instance.regions.all),
             ('Site Groups', instance.site_groups.all),
             ('Site Groups', instance.site_groups.all),
             ('Sites', instance.sites.all),
             ('Sites', instance.sites.all),
+            ('Locations', instance.locations.all),
             ('Device Types', instance.device_types.all),
             ('Device Types', instance.device_types.all),
             ('Roles', instance.roles.all),
             ('Roles', instance.roles.all),
             ('Platforms', instance.platforms.all),
             ('Platforms', instance.platforms.all),
@@ -311,7 +312,6 @@ class ConfigContextView(generic.ObjectView):
 class ConfigContextEditView(generic.ObjectEditView):
 class ConfigContextEditView(generic.ObjectEditView):
     queryset = ConfigContext.objects.all()
     queryset = ConfigContext.objects.all()
     form = forms.ConfigContextForm
     form = forms.ConfigContextForm
-    template_name = 'extras/configcontext_edit.html'
 
 
 
 
 class ConfigContextBulkEditView(generic.BulkEditView):
 class ConfigContextBulkEditView(generic.BulkEditView):

+ 0 - 37
netbox/templates/extras/configcontext_edit.html

@@ -1,37 +0,0 @@
-{% extends 'generic/object_edit.html' %}
-{% load form_helpers %}
-
-{% block form %}
-    <div class="card">
-        <h5 class="card-header">Config Context</h5>
-        <div class="card-body">
-            {% render_field form.name %}
-            {% render_field form.weight %}
-            {% render_field form.description %}
-            {% render_field form.is_active %}
-        </div>
-    </div>
-    <div class="card">
-        <h5 class="card-header">Assignment</h5>
-        <div class="card-body">
-            {% render_field form.regions %}
-            {% render_field form.site_groups %}
-            {% render_field form.sites %}
-            {% render_field form.device_types %}
-            {% render_field form.roles %}
-            {% render_field form.platforms %}
-            {% render_field form.cluster_types %}
-            {% render_field form.cluster_groups %}
-            {% render_field form.clusters %}
-            {% render_field form.tenant_groups %}
-            {% render_field form.tenants %}
-            {% render_field form.tags %}
-        </div>
-    </div>
-    <div class="card">
-        <h5 class="card-header">Data</h5>
-        <div class="card-body">
-            {% render_field form.data %}
-        </div>
-    </div>
-{% endblock %}