Closes #1673: Added object/list views for services

This commit is contained in:
Jeremy Stretch 2018-06-21 15:55:27 -04:00
parent 258373f1a1
commit 7819d9c112
10 changed files with 212 additions and 9 deletions

View File

@ -426,6 +426,10 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class ServiceFilter(django_filters.FilterSet): class ServiceFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
@ -450,3 +454,9 @@ class ServiceFilter(django_filters.FilterSet):
class Meta: class Meta:
model = Service model = Service
fields = ['name', 'protocol', 'port'] 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)

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.core.exceptions import MultipleObjectsReturned from django.core.exceptions import MultipleObjectsReturned
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import Count from django.db.models import Count
from taggit.forms import TagField from taggit.forms import TagField
@ -10,12 +11,14 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
AnnotatedMultipleChoiceField, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, AnnotatedMultipleChoiceField, APISelect, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, ChainedModelChoiceField, CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField,
SlugField, add_blank_choice, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
) )
from virtualization.models import VirtualMachine 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 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
IP_FAMILY_CHOICES = [ IP_FAMILY_CHOICES = [
@ -940,3 +943,25 @@ class ServiceForm(BootstrapMixin, forms.ModelForm):
) )
else: else:
self.fields['ipaddresses'].choices = [] 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']

View File

@ -884,6 +884,9 @@ class Service(CreatedUpdatedModel):
def __str__(self): def __str__(self):
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
def get_absolute_url(self):
return reverse('ipam:service', args=[self.pk])
@property @property
def parent(self): def parent(self):
return self.device or self.virtual_machine return self.device or self.virtual_machine

View File

@ -6,7 +6,7 @@ from django_tables2.utils import Accessor
from dcim.models import Interface from dcim.models import Interface
from tenancy.tables import COL_TENANT from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, ToggleColumn 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 = """ RIR_UTILIZATION = """
<div class="progress"> <div class="progress">
@ -392,3 +392,19 @@ class VLANMemberTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = Interface
fields = ('parent', 'name', 'untagged', 'actions') 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')

View File

@ -85,6 +85,10 @@ urlpatterns = [
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'), url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
# Services # 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<pk>\d+)/$', views.ServiceView.as_view(), name='service'),
url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'), url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'), url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),

View File

@ -931,6 +931,25 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Services # 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): class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_service' permission_required = 'ipam.add_service'
model = Service model = Service
@ -944,9 +963,6 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine']) obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine'])
return obj return obj
def get_return_url(self, request, obj):
return obj.parent.get_absolute_url()
class ServiceEditView(ServiceCreateView): class ServiceEditView(ServiceCreateView):
permission_required = 'ipam.change_service' permission_required = 'ipam.change_service'
@ -955,3 +971,22 @@ class ServiceEditView(ServiceCreateView):
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView): class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_service' permission_required = 'ipam.delete_service'
model = 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'

View File

@ -283,6 +283,11 @@
{% endif %} {% endif %}
<a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a> <a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a>
</li> </li>
<li class="divider"></li>
<li class="dropdown-header">Services</li>
<li>
<a href="{% url 'ipam:service_list' %}">Services</a>
</li>
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|contains:'/virtualization/' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/virtualization/' %} active{% endif %}">

View File

@ -1,5 +1,7 @@
<tr> <tr>
<td>{{ service.name }}</td> <td>
<a href="{{ service.get_absolute_url }}">{{ service.name }}</a>
</td>
<td> <td>
{{ service.get_protocol_display }}/{{ service.port }} {{ service.get_protocol_display }}/{{ service.port }}
</td> </td>

View File

@ -0,0 +1,88 @@
{% extends '_base.html' %}
{% load helpers %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'ipam:service_list' %}">Services</a></li>
<li><a href="{{ service.parent.get_absolute_url }}">{{ service.parent }}</a></li>
<li>{{ service }}</li>
</ol>
</div>
<div class="col-sm-4 col-md-3">
<form action="{% url 'ipam:service_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search Services" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% if perms.dcim.change_service %}
<div class="pull-right">
<a href="{% url 'ipam:service_edit' pk=service.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this service
</a>
</div>
{% endif %}
<h1>{% block title %}{{ service }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=service %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Service</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Name</td>
<td>{{ service.name }}</td>
</tr>
<tr>
<td>Parent</td>
<td>
<a href="{{ service.parent.get_absolute_url }}">{{ service.parent }}</a>
</td>
</tr>
<tr>
<td>Protocol</td>
<td>{{ service.get_protocol_display }}</td>
</tr>
<tr>
<td>Port</td>
<td>{{ service.port }}</td>
</tr>
<tr>
<td>IP Addresses</td>
<td>
{% for ipaddress in service.ipaddresses.all %}
<a href="{{ ipaddress.get_absolute_url }}">{{ ipaddress }}</a><br />
{% empty %}
<span class="text-muted">None</span>
{% endfor %}
</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if service.description %}
{{ service.description }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table>
</div>
{% with service.get_custom_fields as custom_fields %}
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends '_base.html' %}
{% load buttons %}
{% load humanize %}
{% block content %}
<h1>{% block title %}Services{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:service_bulk_edit' bulk_delete_url='ipam:service_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}