diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 57c74cf84..453cead1c 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -117,7 +117,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd required=False, label='ASN' ) - asns = DynamicModelChoiceField( + asns = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(), label=_('ASNs'), required=False diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index a9fdb3652..36c349740 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -172,17 +172,6 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): 'longitude': "Longitude in decimal format (xx.yyyyyy)" } - def __init__(self, data=None, instance=None, *args, **kwargs): - super().__init__(data=data, instance=instance, *args, **kwargs) - - if self.instance and self.instance.pk is not None: - self.fields['asns'].initial = self.instance.asns.all().values_list('id', flat=True) - - def save(self, *args, **kwargs): - instance = super().save(*args, **kwargs) - instance.asns.set(self.cleaned_data['asns']) - return instance - class LocationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): region = DynamicModelChoiceField( diff --git a/netbox/dcim/migrations/0141_asn_model.py b/netbox/dcim/migrations/0141_asn_model.py new file mode 100644 index 000000000..7650679f1 --- /dev/null +++ b/netbox/dcim/migrations/0141_asn_model.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.8 on 2021-11-02 16:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0052_asn_model'), + ('dcim', '0140_wireless'), + ] + + operations = [ + migrations.AddField( + model_name='site', + name='asns', + field=models.ManyToManyField(blank=True, related_name='sites', to='ipam.ASN'), + ), + ] diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index a978e69e6..79f8921d5 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -195,6 +195,11 @@ class Site(PrimaryModel): verbose_name='ASN', help_text='32-bit autonomous system number' ) + asns = models.ManyToManyField( + to='ipam.ASN', + related_name='sites', + blank=True + ) time_zone = TimeZoneField( blank=True ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index d05d2b2f2..0cbd892f5 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -183,7 +183,7 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_asns(self): - params = {'asns': [65001, 65002]} + params = {'asns': [64512, 65002]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_latitude(self): diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index a05f62621..9b8ac3e45 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -310,7 +310,6 @@ class SiteView(generic.ObjectView): def get_extra_context(self, request, instance): stats = { - 'asn_count': ASN.objects.restrict(request.user, 'view').filter(sites=instance).count(), 'rack_count': Rack.objects.restrict(request.user, 'view').filter(site=instance).count(), 'device_count': Device.objects.restrict(request.user, 'view').filter(site=instance).count(), 'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(site=instance).count(), @@ -333,9 +332,15 @@ class SiteView(generic.ObjectView): cumulative=True ).restrict(request.user, 'view').filter(site=instance) + asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance) + asn_count = asns.count() + + stats.update({'asn_count': asn_count}) + return { 'stats': stats, 'locations': locations, + 'asns': asns, } diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index abf2aa4a1..ea00b6914 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -134,14 +134,18 @@ class ASNForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): label='Sites', required=False ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) class Meta: model = ASN fields = [ - 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description' + 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'tags' ] fieldsets = ( - ('ASN', ('asn', 'rir', 'sites', 'description')), + ('ASN', ('asn', 'rir', 'sites', 'description', 'tags')), ('Tenancy', ('tenant_group', 'tenant')), ) help_texts = { @@ -152,6 +156,17 @@ class ASNForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): 'date_added': DatePicker(), } + def __init__(self, data=None, instance=None, *args, **kwargs): + super().__init__(data=data, instance=instance, *args, **kwargs) + + if self.instance and self.instance.pk is not None: + self.fields['sites'].initial = self.instance.sites.all().values_list('id', flat=True) + + def save(self, *args, **kwargs): + instance = super().save(*args, **kwargs) + instance.sites.set(self.cleaned_data['sites']) + return instance + class RoleForm(BootstrapMixin, CustomFieldModelForm): slug = SlugField() diff --git a/netbox/ipam/migrations/0052_asn_model.py b/netbox/ipam/migrations/0052_asn_model.py new file mode 100644 index 000000000..04eac76c3 --- /dev/null +++ b/netbox/ipam/migrations/0052_asn_model.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.8 on 2021-11-02 16:16 + +import dcim.fields +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0004_extend_tag_support'), + ('extras', '0064_configrevision'), + ('ipam', '0051_extend_tag_support'), + ] + + operations = [ + migrations.CreateModel( + name='ASN', + fields=[ + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('asn', dcim.fields.ASNField(unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('rir', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='asns', to='ipam.rir')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='asns', to='tenancy.tenant')), + ], + options={ + 'verbose_name': 'ASN', + 'verbose_name_plural': 'ASNs', + 'ordering': ['asn'], + }, + ), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index d61ad4c25..ad707dda1 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -71,6 +71,7 @@ class RIR(OrganizationalModel): return reverse('ipam:rir', args=[self.pk]) +@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks') class ASN(PrimaryModel): asn = ASNField( @@ -98,11 +99,6 @@ class ASN(PrimaryModel): blank=True, null=True ) - sites = models.ManyToManyField( - to='dcim.Site', - related_name='asns', - blank=True - ) objects = RestrictedQuerySet.as_manager() diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 73b228ac4..7801eec23 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -214,13 +214,10 @@ class ASNView(generic.ObjectView): queryset = ASN.objects.all() def get_extra_context(self, request, instance): - sites_table = SiteTable( - list(instance.sites.all()), - orderable=False - ) + sites = instance.sites.restrict(request.user, 'view').all() return { - 'sites_table': sites_table, + 'sites': sites, } diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 0364dee64..308b09816 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -260,6 +260,20 @@ {% endif %} +
+
+ ASNs +
+
+ {% if asns %} + {% for asn in asns %} + {{ asn }} + {% endfor %} + {% else %} + None + {% endif %} +
+
{% include 'inc/panels/image_attachments.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/ipam/asn.html b/netbox/templates/ipam/asn.html index 8be09c660..8eafe7633 100644 --- a/netbox/templates/ipam/asn.html +++ b/netbox/templates/ipam/asn.html @@ -47,17 +47,30 @@ + {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:asn_list' %} {% plugin_left_page object %}
- {% include 'inc/panels/custom_fields.html' %} - {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:asn_list' %} +
+
+ Sites +
+
+ {% if sites %} + {% for site in sites %} + {{ site }} + {% endfor %} + {% else %} + None + {% endif %} +
+
{% plugin_right_page object %}
- {% include 'inc/panel_table.html' with table=sites_table heading='Sites' %} {% plugin_full_width_page object %}