From ed10a997716b7e430c521c617e9759e6e0224344 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 25 Jan 2018 13:07:04 -0500 Subject: [PATCH] Closes #1758: Added 'status' field to Site model --- netbox/dcim/api/serializers.py | 7 +++--- netbox/dcim/constants.py | 14 ++++++++++-- netbox/dcim/filters.py | 2 +- netbox/dcim/forms.py | 22 +++++++++++++++---- ...e_zone.py => 0054_site_status_timezone.py} | 9 ++++++-- netbox/dcim/models.py | 9 ++++++-- netbox/dcim/tables.py | 11 +++++----- netbox/templates/dcim/site.html | 6 +++++ netbox/templates/dcim/site_edit.html | 1 + 9 files changed, 62 insertions(+), 19 deletions(-) rename netbox/dcim/migrations/{0054_site_time_zone.py => 0054_site_status_timezone.py} (56%) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 4f5df9b79..1debcf1dd 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -8,7 +8,7 @@ from rest_framework.validators import UniqueTogetherValidator from circuits.models import Circuit, CircuitTermination from dcim.constants import ( CONNECTION_STATUS_CHOICES, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_MODE_CHOICES, IFACE_ORDERING_CHOICES, - RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, SUBDEVICE_ROLE_CHOICES, + RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES, ) from dcim.models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -56,6 +56,7 @@ class WritableRegionSerializer(ValidatedModelSerializer): # class SiteSerializer(CustomFieldModelSerializer): + status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES) region = NestedRegionSerializer() tenant = NestedTenantSerializer() time_zone = TimeZoneField(required=False) @@ -63,7 +64,7 @@ class SiteSerializer(CustomFieldModelSerializer): class Meta: model = Site fields = [ - 'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'physical_address', + 'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits', @@ -84,7 +85,7 @@ class WritableSiteSerializer(CustomFieldModelSerializer): class Meta: model = Site fields = [ - 'id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'physical_address', + 'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 5940c4025..cea56e176 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -218,8 +218,18 @@ DEVICE_STATUS_CHOICES = [ [DEVICE_STATUS_INVENTORY, 'Inventory'], ] -# Bootstrap CSS classes for device stasuses -DEVICE_STATUS_CLASSES = { +# Site statuses +SITE_STATUS_ACTIVE = 1 +SITE_STATUS_PLANNED = 2 +SITE_STATUS_RETIRED = 4 +SITE_STATUS_CHOICES = [ + [SITE_STATUS_ACTIVE, 'Active'], + [SITE_STATUS_PLANNED, 'Planned'], + [SITE_STATUS_RETIRED, 'Retired'], +] + +# Bootstrap CSS classes for device statuses +STATUS_CLASSES = { 0: 'warning', 1: 'success', 2: 'info', diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 3f79acf8c..7bf377363 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -67,7 +67,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: model = Site - fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email'] + fields = ['q', 'name', 'slug', 'status', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c4af2e605..8f4e324a4 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -24,7 +24,7 @@ from virtualization.models import Cluster from .constants import ( CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, DEVICE_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_MODE_ACCESS, IFACE_MODE_CHOICES, IFACE_MODE_TAGGED_ALL, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES, - RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, SUBDEVICE_ROLE_CHILD, + RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES, ) from .formfields import MACAddressFormField @@ -104,7 +104,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = Site fields = [ - 'name', 'slug', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'physical_address', + 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone', 'comments', ] widgets = { @@ -121,6 +121,11 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): class SiteCSVForm(forms.ModelForm): + status = CSVChoiceField( + choices=DEVICE_STATUS_CHOICES, + required=False, + help_text='Operational status' + ) region = forms.ModelChoiceField( queryset=Region.objects.all(), required=False, @@ -143,7 +148,7 @@ class SiteCSVForm(forms.ModelForm): class Meta: model = Site fields = [ - 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', + 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone', 'comments', ] help_texts = { @@ -155,6 +160,7 @@ class SiteCSVForm(forms.ModelForm): class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput) + status = forms.ChoiceField(choices=add_blank_choice(SITE_STATUS_CHOICES), required=False, initial='') region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN') @@ -164,9 +170,17 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['region', 'tenant', 'asn', 'time_zone'] +def site_status_choices(): + status_counts = {} + for status in Site.objects.values('status').annotate(count=Count('status')).order_by('status'): + status_counts[status['status']] = status['count'] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in SITE_STATUS_CHOICES] + + class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Site q = forms.CharField(required=False, label='Search') + status = forms.MultipleChoiceField(choices=site_status_choices, required=False) region = FilterTreeNodeMultipleChoiceField( queryset=Region.objects.annotate(filter_count=Count('sites')), to_field_name='slug', @@ -889,7 +903,7 @@ class BaseDeviceCSVForm(forms.ModelForm): ) status = CSVChoiceField( choices=DEVICE_STATUS_CHOICES, - help_text='Operational status of device' + help_text='Operational status' ) class Meta: diff --git a/netbox/dcim/migrations/0054_site_time_zone.py b/netbox/dcim/migrations/0054_site_status_timezone.py similarity index 56% rename from netbox/dcim/migrations/0054_site_time_zone.py rename to netbox/dcim/migrations/0054_site_status_timezone.py index f599cb155..56fbfcebc 100644 --- a/netbox/dcim/migrations/0054_site_time_zone.py +++ b/netbox/dcim/migrations/0054_site_status_timezone.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.6 on 2017-12-19 21:53 +# Generated by Django 1.11.6 on 2018-01-25 17:57 from __future__ import unicode_literals -from django.db import migrations +from django.db import migrations, models import timezone_field.fields @@ -13,6 +13,11 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AddField( + model_name='site', + name='status', + field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1), + ), migrations.AddField( model_name='site', name='time_zone', diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index d19ce7a49..099291e95 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -83,6 +83,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): """ name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) + status = models.PositiveSmallIntegerField(choices=SITE_STATUS_CHOICES, default=SITE_STATUS_ACTIVE) region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL) tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT) facility = models.CharField(max_length=50, blank=True) @@ -100,7 +101,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): objects = SiteManager() csv_headers = [ - 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'contact_name', 'contact_phone', + 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'contact_name', 'contact_phone', 'contact_email', ] @@ -117,6 +118,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): return csv_format([ self.name, self.slug, + self.get_status_display(), self.region.name if self.region else None, self.tenant.name if self.tenant else None, self.facility, @@ -127,6 +129,9 @@ class Site(CreatedUpdatedModel, CustomFieldModel): self.contact_email, ]) + def get_status_class(self): + return STATUS_CLASSES[self.status] + @property def count_prefixes(self): return self.prefixes.count() @@ -1088,7 +1093,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): return Device.objects.filter(parent_bay__device=self.pk) def get_status_class(self): - return DEVICE_STATUS_CLASSES[self.status] + return STATUS_CLASSES[self.status] def get_rpc_client(self): """ diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 55dbeb8c4..870a64bac 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -92,7 +92,7 @@ DEVICE_ROLE = """ """ -DEVICE_STATUS = """ +STATUS_LABEL = """ {{ record.get_status_display }} """ @@ -145,12 +145,13 @@ class RegionTable(BaseTable): class SiteTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() + status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') region = tables.TemplateColumn(template_code=SITE_REGION_LINK) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) class Meta(BaseTable.Meta): model = Site - fields = ('pk', 'name', 'facility', 'region', 'tenant', 'asn') + fields = ('pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn') class SiteDetailTable(SiteTable): @@ -163,7 +164,7 @@ class SiteDetailTable(SiteTable): class Meta(SiteTable.Meta): fields = ( - 'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', + 'pk', 'name', 'status', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', 'vlan_count', 'circuit_count', 'vm_count', ) @@ -409,7 +410,7 @@ class PlatformTable(BaseTable): class DeviceTable(BaseTable): pk = ToggleColumn() name = tables.TemplateColumn(template_code=DEVICE_LINK) - status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status') + status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')]) @@ -436,7 +437,7 @@ class DeviceDetailTable(DeviceTable): class DeviceImportTable(BaseTable): name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') - status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status') + status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack') diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index c61f007df..ad7f0276d 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -58,6 +58,12 @@ Site + + + +
Status + {{ site.get_status_display }} +
Region diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index 21b78f229..582f93996 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -7,6 +7,7 @@
{% render_field form.name %} {% render_field form.slug %} + {% render_field form.status %} {% render_field form.region %} {% render_field form.facility %} {% render_field form.asn %}