diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index ebd1fe28d..dd07b8333 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -26,37 +26,37 @@ __all__ = ( class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='circuits__terminations__site__region', + field_name='circuits__terminations___site__region', lookup_expr='in', label=_('Region (ID)'), ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='circuits__terminations__site__region', + field_name='circuits__terminations___site__region', lookup_expr='in', to_field_name='slug', label=_('Region (slug)'), ) site_group_id = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), - field_name='circuits__terminations__site__group', + field_name='circuits__terminations___site__group', lookup_expr='in', label=_('Site group (ID)'), ) site_group = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), - field_name='circuits__terminations__site__group', + field_name='circuits__terminations___site__group', lookup_expr='in', to_field_name='slug', label=_('Site group (slug)'), ) site_id = django_filters.ModelMultipleChoiceFilter( - field_name='circuits__terminations__site', + field_name='circuits__terminations___site', queryset=Site.objects.all(), label=_('Site'), ) site = django_filters.ModelMultipleChoiceFilter( - field_name='circuits__terminations__site__slug', + field_name='circuits__terminations___site__slug', queryset=Site.objects.all(), to_field_name='slug', label=_('Site (slug)'), @@ -193,37 +193,37 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte ) region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='terminations__site__region', + field_name='terminations___site__region', lookup_expr='in', label=_('Region (ID)'), ) region = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), - field_name='terminations__site__region', + field_name='terminations___site__region', lookup_expr='in', to_field_name='slug', label=_('Region (slug)'), ) site_group_id = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), - field_name='terminations__site__group', + field_name='terminations___site__group', lookup_expr='in', label=_('Site group (ID)'), ) site_group = TreeNodeMultipleChoiceFilter( queryset=SiteGroup.objects.all(), - field_name='terminations__site__group', + field_name='terminations___site__group', lookup_expr='in', to_field_name='slug', label=_('Site group (slug)'), ) site_id = django_filters.ModelMultipleChoiceFilter( - field_name='terminations__site', + field_name='terminations___site', queryset=Site.objects.all(), label=_('Site (ID)'), ) site = django_filters.ModelMultipleChoiceFilter( - field_name='terminations__site__slug', + field_name='terminations___site__slug', queryset=Site.objects.all(), to_field_name='slug', label=_('Site (slug)'), @@ -263,16 +263,16 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet): queryset=Circuit.objects.all(), label=_('Circuit'), ) - site_id = django_filters.ModelMultipleChoiceFilter( - queryset=Site.objects.all(), - label=_('Site (ID)'), - ) - site = django_filters.ModelMultipleChoiceFilter( - field_name='site__slug', - queryset=Site.objects.all(), - to_field_name='slug', - label=_('Site (slug)'), - ) + # site_id = django_filters.ModelMultipleChoiceFilter( + # queryset=Site.objects.all(), + # label=_('Site (ID)'), + # ) + # site = django_filters.ModelMultipleChoiceFilter( + # field_name='site__slug', + # queryset=Site.objects.all(), + # to_field_name='slug', + # label=_('Site (slug)'), + # ) provider_network_id = django_filters.ModelMultipleChoiceFilter( queryset=ProviderNetwork.objects.all(), label=_('ProviderNetwork (ID)'), diff --git a/netbox/circuits/migrations/0046_circuittermination_scope_id_and_more.py b/netbox/circuits/migrations/0046_circuittermination__scope.py similarity index 57% rename from netbox/circuits/migrations/0046_circuittermination_scope_id_and_more.py rename to netbox/circuits/migrations/0046_circuittermination__scope.py index 5284ca93f..482ba1f47 100644 --- a/netbox/circuits/migrations/0046_circuittermination_scope_id_and_more.py +++ b/netbox/circuits/migrations/0046_circuittermination__scope.py @@ -1,14 +1,27 @@ -# Generated by Django 5.0.9 on 2024-10-21 17:15 - import django.db.models.deletion from django.db import migrations, models +def copy_site_assignments(apps, schema_editor): + """ + Copy site ForeignKey values to the scope GFK. + """ + ContentType = apps.get_model('contenttypes', 'ContentType') + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + Site = apps.get_model('dcim', 'Site') + + CircuitTermination.objects.filter(site__isnull=False).update( + scope_type=ContentType.objects.get_for_model(Site), + scope_id=models.F('site_id') + ) + + class Migration(migrations.Migration): dependencies = [ ('circuits', '0045_circuit_distance'), ('contenttypes', '0002_remove_content_type_name'), + ('dcim', '0193_poweroutlet_color'), ] operations = [ @@ -29,4 +42,10 @@ class Migration(migrations.Migration): to='contenttypes.contenttype', ), ), + + # Copy over existing site assignments + migrations.RunPython( + code=copy_site_assignments, + reverse_code=migrations.RunPython.noop + ), ] diff --git a/netbox/circuits/migrations/0047_circuitterminations_cached_relations.py b/netbox/circuits/migrations/0047_circuitterminations_cached_relations.py new file mode 100644 index 000000000..9baa36f23 --- /dev/null +++ b/netbox/circuits/migrations/0047_circuitterminations_cached_relations.py @@ -0,0 +1,85 @@ +# Generated by Django 5.0.9 on 2024-10-21 17:34 +import django.db.models.deletion +from django.db import migrations, models + + +def populate_denormalized_fields(apps, schema_editor): + """ + Copy site ForeignKey values to the scope GFK. + """ + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + + terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site') + for termination in terminations: + termination._region_id = termination.site.region_id + termination._sitegroup_id = termination.site.group_id + termination._site_id = termination.site_id + # Note: Location cannot be set prior to migration + + CircuitTermination.objects.bulk_update(terminations, ['_region', '_sitegroup', '_site']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0046_circuittermination__scope'), + ] + + operations = [ + migrations.AddField( + model_name='circuittermination', + name='_location', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='_circuit_terminations', + to='dcim.location', + ), + ), + migrations.AddField( + model_name='circuittermination', + name='_region', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='_circuit_terminations', + to='dcim.region', + ), + ), + migrations.AddField( + model_name='circuittermination', + name='_site', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='_circuit_terminations', + to='dcim.site', + ), + ), + migrations.AddField( + model_name='circuittermination', + name='_sitegroup', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='_circuit_terminations', + to='dcim.sitegroup', + ), + ), + + # Populate denormalized FK values + migrations.RunPython( + code=populate_denormalized_fields, + reverse_code=migrations.RunPython.noop + ), + + # Delete the site ForeignKey + migrations.RemoveField( + model_name='circuittermination', + name='site', + ), + ] diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 01f35ea68..19dae9e1f 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -250,13 +250,13 @@ class CircuitTermination( ct_field='scope_type', fk_field='scope_id' ) - site = models.ForeignKey( - to='dcim.Site', - on_delete=models.PROTECT, - related_name='circuit_terminations', - blank=True, - null=True - ) + # site = models.ForeignKey( + # to='dcim.Site', + # on_delete=models.PROTECT, + # related_name='circuit_terminations', + # blank=True, + # null=True + # ) provider_network = models.ForeignKey( to='circuits.ProviderNetwork', on_delete=models.PROTECT, @@ -294,6 +294,36 @@ class CircuitTermination( blank=True ) + # Cached associations to enable efficient filtering + _location = models.ForeignKey( + to='dcim.Location', + on_delete=models.CASCADE, + related_name='_circuit_terminations', + blank=True, + null=True + ) + _site = models.ForeignKey( + to='dcim.Site', + on_delete=models.CASCADE, + related_name='_circuit_terminations', + blank=True, + null=True + ) + _region = models.ForeignKey( + to='dcim.Region', + on_delete=models.CASCADE, + related_name='_circuit_terminations', + blank=True, + null=True + ) + _sitegroup = models.ForeignKey( + to='dcim.SiteGroup', + on_delete=models.CASCADE, + related_name='_circuit_terminations', + blank=True, + null=True + ) + class Meta: ordering = ['circuit', 'term_side'] constraints = (