From 258373f1a1b09097655790f32ffd2455db956a05 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Jun 2018 14:55:10 -0400 Subject: [PATCH 1/3] Closes #2118: Added latitude and longitude fields to Site --- netbox/dcim/api/serializers.py | 8 +++--- netbox/dcim/forms.py | 8 +++--- .../0060_site_latitude_longitude.py | 25 +++++++++++++++++++ netbox/dcim/models.py | 16 +++++++++++- netbox/templates/dcim/site.html | 10 ++++++++ netbox/templates/dcim/site_edit.html | 2 ++ 6 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 netbox/dcim/migrations/0060_site_latitude_longitude.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 8a8824138..d0e134b71 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from collections import OrderedDict - from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from taggit.models import Tag @@ -63,9 +61,9 @@ class SiteSerializer(CustomFieldModelSerializer): model = Site fields = [ 'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', - 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes', 'count_vlans', 'count_racks', - 'count_devices', 'count_circuits', + 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', + 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes', + 'count_vlans', 'count_racks', 'count_devices', 'count_circuits', ] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6460d5d2c..807caa5ce 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -115,8 +115,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): model = Site fields = [ 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description', - 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', - 'tags', + 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', + 'contact_email', 'comments', 'tags', ] widgets = { 'physical_address': SmallTextarea(attrs={'rows': 3}), @@ -129,7 +129,9 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): 'time_zone': "Local time zone", 'description': "Short description (will appear in sites list)", 'physical_address': "Physical location of the building (e.g. for GPS)", - 'shipping_address': "If different from the physical address" + 'shipping_address': "If different from the physical address", + 'latitude': "Latitude in decimal format (xx.yyyyyy)", + 'longitude': "Longitude in decimal format (xx.yyyyyy)" } diff --git a/netbox/dcim/migrations/0060_site_latitude_longitude.py b/netbox/dcim/migrations/0060_site_latitude_longitude.py new file mode 100644 index 000000000..750a0f10b --- /dev/null +++ b/netbox/dcim/migrations/0060_site_latitude_longitude.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-06-21 18:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0059_devicetype_add_created_updated_times'), + ] + + operations = [ + migrations.AddField( + model_name='site', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True), + ), + migrations.AddField( + model_name='site', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1209153bc..9a0587485 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -135,6 +135,18 @@ class Site(CreatedUpdatedModel, CustomFieldModel): max_length=200, blank=True ) + latitude = models.DecimalField( + max_digits=8, + decimal_places=6, + blank=True, + null=True + ) + longitude = models.DecimalField( + max_digits=9, + decimal_places=6, + blank=True, + null=True + ) contact_name = models.CharField( max_length=50, blank=True @@ -164,7 +176,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel): csv_headers = [ 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', - 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', + 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments', ] serializer = 'dcim.api.serializers.SiteSerializer' @@ -191,6 +203,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel): self.description, self.physical_address, self.shipping_address, + self.latitude, + self.longitude, self.contact_name, self.contact_phone, self.contact_email, diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index a882d77c8..6e53f6716 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -175,6 +175,16 @@ {% endif %} + + GPS Coordinates + + {% if site.latitude and site.longitude %} + {{ site.latitude }}, {{ site.longitude }} + {% else %} + N/A + {% endif %} + + Contact Name diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index ad7932642..a8d14aca7 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -27,6 +27,8 @@
{% render_field form.physical_address %} {% render_field form.shipping_address %} + {% render_field form.latitude %} + {% render_field form.longitude %} {% render_field form.contact_name %} {% render_field form.contact_phone %} {% render_field form.contact_email %} From 7819d9c112ec539d674c46ff7b2778825244e50a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Jun 2018 15:55:27 -0400 Subject: [PATCH 2/3] Closes #1673: Added object/list views for services --- netbox/ipam/filters.py | 10 +++ netbox/ipam/forms.py | 33 ++++++++-- netbox/ipam/models.py | 3 + netbox/ipam/tables.py | 18 ++++- netbox/ipam/urls.py | 4 ++ netbox/ipam/views.py | 41 +++++++++++- netbox/templates/inc/nav_menu.html | 5 ++ netbox/templates/ipam/inc/service.html | 4 +- netbox/templates/ipam/service.html | 88 +++++++++++++++++++++++++ netbox/templates/ipam/service_list.html | 15 +++++ 10 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 netbox/templates/ipam/service.html create mode 100644 netbox/templates/ipam/service_list.html diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index db2806b77..487970b49 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -426,6 +426,10 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): class ServiceFilter(django_filters.FilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) device_id = django_filters.ModelMultipleChoiceFilter( queryset=Device.objects.all(), label='Device (ID)', @@ -450,3 +454,9 @@ class ServiceFilter(django_filters.FilterSet): class Meta: model = Service fields = ['name', 'protocol', 'port'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + qs_filter = Q(name__icontains=value) | Q(description__icontains=value) + return queryset.filter(qs_filter) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 82ebfe724..a521549f9 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django import forms from django.core.exceptions import MultipleObjectsReturned +from django.core.validators import MaxValueValidator, MinValueValidator from django.db.models import Count from taggit.forms import TagField @@ -10,12 +11,14 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( - AnnotatedMultipleChoiceField, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, - CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, - SlugField, add_blank_choice, + AnnotatedMultipleChoiceField, APISelect, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, + ChainedModelChoiceField, CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, + Livesearch, ReturnURLForm, SlugField, add_blank_choice, ) from virtualization.models import VirtualMachine -from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES +from .constants import ( + IP_PROTOCOL_CHOICES, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES, +) from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF IP_FAMILY_CHOICES = [ @@ -940,3 +943,25 @@ class ServiceForm(BootstrapMixin, forms.ModelForm): ) else: self.fields['ipaddresses'].choices = [] + + +class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm): + model = Service + q = forms.CharField(required=False, label='Search') + protocol = forms.ChoiceField( + choices=add_blank_choice(IP_PROTOCOL_CHOICES), + required=False + ) + port = forms.IntegerField( + required=False + ) + + +class ServiceBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), widget=forms.MultipleHiddenInput) + protocol = forms.ChoiceField(choices=add_blank_choice(IP_PROTOCOL_CHOICES), required=False) + port = forms.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)], required=False) + description = forms.CharField(max_length=100, required=False) + + class Meta: + nullable_fields = ['site', 'group', 'tenant', 'role', 'description'] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index a3d8736c1..736160fd8 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -884,6 +884,9 @@ class Service(CreatedUpdatedModel): def __str__(self): return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) + def get_absolute_url(self): + return reverse('ipam:service', args=[self.pk]) + @property def parent(self): return self.device or self.virtual_machine diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 08035d549..7f8b8918d 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -6,7 +6,7 @@ from django_tables2.utils import Accessor from dcim.models import Interface from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, ToggleColumn -from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF +from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF RIR_UTILIZATION = """
@@ -392,3 +392,19 @@ class VLANMemberTable(BaseTable): class Meta(BaseTable.Meta): model = Interface fields = ('parent', 'name', 'untagged', 'actions') + + +# +# Services +# + +class ServiceTable(BaseTable): + pk = ToggleColumn() + name = tables.LinkColumn( + viewname='ipam:service', + args=[Accessor('pk')] + ) + + class Meta(BaseTable.Meta): + model = Service + fields = ('pk', 'name', 'parent', 'protocol', 'port', 'description') diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index aa7c17a5c..d22c32561 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -85,6 +85,10 @@ urlpatterns = [ url(r'^vlans/(?P\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'), # Services + url(r'^services/$', views.ServiceListView.as_view(), name='service_list'), + url(r'^services/edit/$', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'), + url(r'^services/delete/$', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'), + url(r'^services/(?P\d+)/$', views.ServiceView.as_view(), name='service'), url(r'^services/(?P\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'), url(r'^services/(?P\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 1d4575e34..e60d1bd79 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -931,6 +931,25 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Services # +class ServiceListView(ObjectListView): + queryset = Service.objects.select_related('device', 'virtual_machine') + filter = filters.ServiceFilter + filter_form = forms.ServiceFilterForm + table = tables.ServiceTable + template_name = 'ipam/service_list.html' + + +class ServiceView(View): + + def get(self, request, pk): + + service = get_object_or_404(Service, pk=pk) + + return render(request, 'ipam/service.html', { + 'service': service, + }) + + class ServiceCreateView(PermissionRequiredMixin, ObjectEditView): permission_required = 'ipam.add_service' model = Service @@ -944,9 +963,6 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView): obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine']) return obj - def get_return_url(self, request, obj): - return obj.parent.get_absolute_url() - class ServiceEditView(ServiceCreateView): permission_required = 'ipam.change_service' @@ -955,3 +971,22 @@ class ServiceEditView(ServiceCreateView): class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_service' model = Service + + +class ServiceBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'ipam.change_service' + cls = Service + queryset = Service.objects.all() + filter = filters.ServiceFilter + table = tables.ServiceTable + form = forms.ServiceBulkEditForm + default_return_url = 'ipam:service_list' + + +class ServiceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): + permission_required = 'ipam.delete_service' + cls = Service + queryset = Service.objects.all() + filter = filters.ServiceFilter + table = tables.ServiceTable + default_return_url = 'ipam:service_list' diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 2c47ad85b..7b9e6ac3c 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -283,6 +283,11 @@ {% endif %} VLAN Groups +
  • + +
  • + Services +
  • + {% if form.custom_fields %} +
    +
    Custom Fields
    +
    + {% render_custom_fields form %} +
    +
    + {% endif %} {% endblock %} diff --git a/netbox/templates/ipam/service_list.html b/netbox/templates/ipam/service_list.html index 840b071bd..c78b2bba2 100644 --- a/netbox/templates/ipam/service_list.html +++ b/netbox/templates/ipam/service_list.html @@ -1,6 +1,4 @@ {% extends '_base.html' %} -{% load buttons %} -{% load humanize %} {% block content %}

    {% block title %}Services{% endblock %}