mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-13 19:18:16 -06:00
ip phone and partition initial
This commit is contained in:
parent
63ad93afd0
commit
ee6bf3c2d6
@ -138,6 +138,7 @@ IFACE_TYPE_SUMMITSTACK512 = 5330
|
||||
|
||||
# Other
|
||||
IFACE_TYPE_OTHER = 32767
|
||||
IFACE_TYPE_PHONE = 32700
|
||||
|
||||
IFACE_TYPE_CHOICES = [
|
||||
[
|
||||
@ -249,6 +250,7 @@ IFACE_TYPE_CHOICES = [
|
||||
'Other',
|
||||
[
|
||||
[IFACE_TYPE_OTHER, 'Other'],
|
||||
[IFACE_TYPE_PHONE, 'Phone'],
|
||||
]
|
||||
],
|
||||
]
|
||||
|
0
netbox/ipphone/__init__.py
Normal file
0
netbox/ipphone/__init__.py
Normal file
3
netbox/ipphone/admin.py
Normal file
3
netbox/ipphone/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
35
netbox/ipphone/api/nested_serializers.py
Normal file
35
netbox/ipphone/api/nested_serializers.py
Normal file
@ -0,0 +1,35 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from ipphone.models import Phone
|
||||
from utilities.api import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedPhoneSerializer',
|
||||
# 'NestedVRFSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
|
||||
# class NestedVRFSerializer(WritableNestedSerializer):
|
||||
# url = serializers.HyperlinkedIdentityField(view_name='ipphone-api:vrf-detail')
|
||||
# prefix_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# class Meta:
|
||||
# model = VRF
|
||||
# fields = ['id', 'url', 'name', 'rd', 'prefix_count']
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
|
||||
class NestedPhoneSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipphone-api:phone-detail')
|
||||
|
||||
class Meta:
|
||||
model = Phone
|
||||
fields = ['id', 'ipphonepartition', 'phone_number']
|
62
netbox/ipphone/api/serializers.py
Normal file
62
netbox/ipphone/api/serializers.py
Normal file
@ -0,0 +1,62 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||
from dcim.models import Interface
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from ipphone.constants import *
|
||||
from ipphone.models import Phone, IPPhonePartition
|
||||
from utilities.api import (
|
||||
ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
|
||||
)
|
||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||
from .nested_serializers import *
|
||||
|
||||
#
|
||||
# IPPhonePartitions
|
||||
#
|
||||
|
||||
class IPPhonePartitionSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = IPPhonePartition
|
||||
fields = [
|
||||
'id', 'name', 'enforce_unique', 'description', 'tags', 'custom_fields',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Phone Numbers
|
||||
#
|
||||
|
||||
class PhoneInterfaceSerializer(WritableNestedSerializer):
|
||||
url = serializers.SerializerMethodField()
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name',
|
||||
]
|
||||
|
||||
def get_url(self, obj):
|
||||
url_name = 'dcim-api:interface-detail'
|
||||
return reverse(url_name, kwargs={'pk': obj.pk}, request=self.context['request'])
|
||||
|
||||
|
||||
class PhoneSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
status = ChoiceField(choices=PHONE_STATUS_CHOICES, required=False)
|
||||
interface = PhoneInterfaceSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Phone
|
||||
fields = [
|
||||
'id', 'ipphonepartition', 'phone_number', 'status', 'interface', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
27
netbox/ipphone/api/urls.py
Normal file
27
netbox/ipphone/api/urls.py
Normal file
@ -0,0 +1,27 @@
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
class IPPHONERootView(routers.APIRootView):
|
||||
"""
|
||||
IPPHONE API root view
|
||||
"""
|
||||
def get_view_name(self):
|
||||
return 'IPPHONE'
|
||||
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.APIRootView = IPPHONERootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.IPPHONEFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# IPPhonePartitions
|
||||
router.register(r'ipphonepartitions', views.IPPhonePartitionViewSet)
|
||||
|
||||
# IP addresses
|
||||
router.register(r'phone_numbers', views.PhoneViewSet)
|
||||
|
||||
app_name = 'ipphone-api'
|
||||
urlpatterns = router.urls
|
45
netbox/ipphone/api/views.py
Normal file
45
netbox/ipphone/api/views.py
Normal file
@ -0,0 +1,45 @@
|
||||
from django.conf import settings
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from ipphone import filters
|
||||
from ipphone.models import Phone, IPPhonePartition
|
||||
from utilities.api import FieldChoicesViewSet, ModelViewSet
|
||||
from utilities.utils import get_subquery
|
||||
from . import serializers
|
||||
|
||||
#
|
||||
# Field choices
|
||||
#
|
||||
|
||||
class IPPHONEFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Phone, ['phone_number'], ['ipphonepartition']),
|
||||
(IPPhonePartition, ['name'], ['enforce_unique']),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Phone Numbers
|
||||
#
|
||||
|
||||
class PhoneViewSet(CustomFieldModelViewSet):
|
||||
queryset = Phone.objects.prefetch_related(
|
||||
'interface__device__device_type', 'tags'
|
||||
)
|
||||
serializer_class = serializers.PhoneSerializer
|
||||
filterset_class = filters.PhoneFilter
|
||||
|
||||
#
|
||||
# IPPhonePartitions
|
||||
#
|
||||
|
||||
class IPPhonePartitionViewSet(CustomFieldModelViewSet):
|
||||
queryset = IPPhonePartition.objects.all()
|
||||
serializer_class = serializers.IPPhonePartitionSerializer
|
||||
filterset_class = filters.IPPhonePartitionFilter
|
5
netbox/ipphone/apps.py
Normal file
5
netbox/ipphone/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IpphoneConfig(AppConfig):
|
||||
name = 'ipphone'
|
18
netbox/ipphone/constants.py
Normal file
18
netbox/ipphone/constants.py
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
# PHONE statuses
|
||||
PHONE_STATUS_ACTIVE = 1
|
||||
PHONE_STATUS_INACTIVE = 2
|
||||
PHONE_STATUS_CHOICES = (
|
||||
(PHONE_STATUS_ACTIVE, 'Active'),
|
||||
(PHONE_STATUS_INACTIVE, 'Inactive')
|
||||
)
|
||||
|
||||
# Bootstrap CSS classes
|
||||
STATUS_CHOICE_CLASSES = {
|
||||
0: 'default',
|
||||
1: 'primary',
|
||||
2: 'info',
|
||||
3: 'danger',
|
||||
4: 'warning',
|
||||
5: 'success',
|
||||
}
|
96
netbox/ipphone/filters.py
Normal file
96
netbox/ipphone/filters.py
Normal file
@ -0,0 +1,96 @@
|
||||
import django_filters
|
||||
import netaddr
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
|
||||
from .constants import PHONE_STATUS_CHOICES
|
||||
from .models import Phone, IPPhonePartition
|
||||
|
||||
class PhoneFilter(CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
parent = django_filters.CharFilter(
|
||||
method='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
phone_number = django_filters.CharFilter(
|
||||
method='filter_phone_number',
|
||||
label='PhoneNumber',
|
||||
)
|
||||
ipphonepartition_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=IPPhonePartition.objects.all(),
|
||||
label='IPPhonePartition',
|
||||
)
|
||||
interface = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='interface__name',
|
||||
queryset=Interface.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Interface (ID)',
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface (ID)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=PHONE_STATUS_CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
class Meta:
|
||||
model = Phone
|
||||
fields = ['phone_number', 'ipphonepartition']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(description__icontains=value) |
|
||||
Q(phone_number__istartswith=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def search_by_parent(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
query = str(value.strip())
|
||||
return queryset.filter(phone_number_contained=query)
|
||||
except (AddrFormatError, ValueError):
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class IPPhonePartitionFilter(TenancyFilterSet, CustomFieldFilterSet):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
)
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
tag = TagFilter()
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPPhonePartition
|
||||
fields = ['name', 'enforce_unique']
|
314
netbox/ipphone/forms.py
Normal file
314
netbox/ipphone/forms.py
Normal file
@ -0,0 +1,314 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from taggit.forms import TagField
|
||||
|
||||
from dcim.models import Site, Rack, Device, Interface
|
||||
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
|
||||
CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, SlugField,
|
||||
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from .constants import PHONE_STATUS_CHOICES
|
||||
from .models import Phone, IPPhonePartition
|
||||
|
||||
|
||||
#
|
||||
# IPPhonePartitions
|
||||
#
|
||||
|
||||
class IPPhonePartitionForm(BootstrapMixin, CustomFieldForm):
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPPhonePartition
|
||||
fields = [
|
||||
'name', 'enforce_unique', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class IPPhonePartitionCSVForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = IPPhonePartition
|
||||
fields = IPPhonePartition.csv_headers
|
||||
help_texts = {
|
||||
'name': 'IP Phone Partition name',
|
||||
}
|
||||
|
||||
|
||||
class IPPhonePartitionBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=IPPhonePartition.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
enforce_unique = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect(),
|
||||
label='Enforce unique space'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
class IPPhonePartitionFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = IPPhonePartition
|
||||
field_order = ['q']
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Phone
|
||||
#
|
||||
|
||||
class PhoneForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
|
||||
interface = forms.ModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/sites/",
|
||||
filter_for={
|
||||
'device': 'site_id'
|
||||
}
|
||||
)
|
||||
)
|
||||
device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/',
|
||||
display_field='display_name',
|
||||
filter_for={
|
||||
'phone_number': 'device_id'
|
||||
}
|
||||
)
|
||||
)
|
||||
# phone_number = ChainedModelChoiceField(
|
||||
# queryset=Phone.objects.all(),
|
||||
# chains=(
|
||||
# ('interface__device', 'device'),
|
||||
# ),
|
||||
# required=False,
|
||||
# label='Phone Number',
|
||||
# widget=APISelect(
|
||||
# api_url='/api/ipphone/phones/',
|
||||
# display_field='phone_number'
|
||||
# )
|
||||
# )
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Phone
|
||||
fields = [
|
||||
'phone_number', 'ipphonepartition', 'status', 'description', 'interface', 'site', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect2()
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
if instance and instance.phone_number is not None:
|
||||
initial['phone_number'] = instance.phone_number
|
||||
else:
|
||||
initial['phone_number'] = ''
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# Limit interface selections to those belonging to the parent device/VM
|
||||
if self.instance and self.instance.interface:
|
||||
self.fields['interface'].queryset = Interface.objects.filter(
|
||||
device=self.instance.interface.device
|
||||
)
|
||||
else:
|
||||
self.fields['interface'].choices = []
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
phone = super().save(*args, **kwargs)
|
||||
|
||||
# Assign/clear this Phone Number as the primary for the associated Device.
|
||||
# parent = self.cleaned_data['interface'].parent
|
||||
phone.save()
|
||||
|
||||
return phone
|
||||
|
||||
|
||||
class PhoneBulkCreateForm(BootstrapMixin, forms.Form):
|
||||
# pattern = ExpandablePhoneField(
|
||||
# label='Phone pattern'
|
||||
# )
|
||||
pattern = ''
|
||||
|
||||
|
||||
class PhoneBulkAddForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = Phone
|
||||
fields = [
|
||||
'phone_number', 'ipphonepartition', 'status', 'description'
|
||||
]
|
||||
widgets = {
|
||||
'ipphonepartition': APISelect(
|
||||
api_url="/api/ipphone/ipphonepartitions/"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class PhoneCSVForm(forms.ModelForm):
|
||||
|
||||
status = CSVChoiceField(
|
||||
choices=PHONE_STATUS_CHOICES,
|
||||
help_text='Operational status'
|
||||
)
|
||||
device = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name or ID of assigned device',
|
||||
error_messages={
|
||||
'invalid_choice': 'Device not found.',
|
||||
}
|
||||
)
|
||||
interface_name = forms.CharField(
|
||||
help_text='Name of assigned interface',
|
||||
required=False
|
||||
)
|
||||
ipphonepartition = FlexibleModelChoiceField(
|
||||
queryset=IPPhonePartition.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Parent IP Phone Partition (or {ID})',
|
||||
error_messages={
|
||||
'invalid_choice': 'IP Phone Partition not found.',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Phone
|
||||
fields = Phone.csv_headers
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
device = self.cleaned_data.get('device')
|
||||
interface_name = self.cleaned_data.get('interface_name')
|
||||
|
||||
# Validate interface
|
||||
if interface_name and device:
|
||||
try:
|
||||
self.instance.interface = Interface.objects.get(device=device, name=interface_name)
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface {} for device {}".format(
|
||||
interface_name, device
|
||||
))
|
||||
elif interface_name:
|
||||
raise forms.ValidationError("Interface given ({}) but parent device not specified".format(
|
||||
interface_name
|
||||
))
|
||||
elif device:
|
||||
raise forms.ValidationError("Device specified ({}) but interface missing".format(device))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set interface
|
||||
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
|
||||
self.instance.interface = Interface.objects.get(
|
||||
device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['interface_name']
|
||||
)
|
||||
|
||||
phone_number = super().save(*args, **kwargs)
|
||||
phone_number.save()
|
||||
# parent = self.cleaned_data['device']
|
||||
# parent.save()
|
||||
|
||||
return phone_number
|
||||
|
||||
|
||||
class PhoneBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=Phone.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(PHONE_STATUS_CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
class PhoneAssignForm(BootstrapMixin, forms.Form):
|
||||
phone_number = forms.CharField(
|
||||
label='Phone Number'
|
||||
)
|
||||
|
||||
|
||||
class PhoneFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Phone
|
||||
field_order = [
|
||||
'q', 'parent', 'ipphonepartition', 'status'
|
||||
]
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
# parent = forms.CharField(
|
||||
# required=False,
|
||||
# widget=forms.TextInput(
|
||||
# attrs={
|
||||
# 'placeholder': 'Prefix',
|
||||
# }
|
||||
# ),
|
||||
# label='Parent Prefix'
|
||||
# )
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=PHONE_STATUS_CHOICES,
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
|
36
netbox/ipphone/migrations/0001_initial.py
Normal file
36
netbox/ipphone/migrations/0001_initial.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-04 17:40
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0075_cable_devices'),
|
||||
('extras', '0025_objectchange_time_index'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Phone',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('phone_number', models.CharField(max_length=15)),
|
||||
('status', models.PositiveSmallIntegerField(default=1)),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('interface', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='phone', to='dcim.Interface')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Phone Number',
|
||||
'verbose_name_plural': 'Phone Numbers',
|
||||
'ordering': ['id', 'phone_number'],
|
||||
},
|
||||
),
|
||||
]
|
37
netbox/ipphone/migrations/0002_auto_20191004_1636.py
Normal file
37
netbox/ipphone/migrations/0002_auto_20191004_1636.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-04 22:36
|
||||
|
||||
from django.db import migrations, models
|
||||
import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0025_objectchange_time_index'),
|
||||
('ipphone', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='phone',
|
||||
name='phone_number',
|
||||
field=models.CharField(max_length=25),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IPPhonePartition',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('enforce_unique', models.BooleanField(default=True)),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'IPPhonePartition',
|
||||
'verbose_name_plural': 'IPPhonePartitions',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
19
netbox/ipphone/migrations/0003_phone_ipphonepartition.py
Normal file
19
netbox/ipphone/migrations/0003_phone_ipphonepartition.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-07 17:16
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipphone', '0002_auto_20191004_1636'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='phone',
|
||||
name='ipphonepartition',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipphone.IPPhonePartition'),
|
||||
),
|
||||
]
|
0
netbox/ipphone/migrations/__init__.py
Normal file
0
netbox/ipphone/migrations/__init__.py
Normal file
185
netbox/ipphone/models.py
Normal file
185
netbox/ipphone/models.py
Normal file
@ -0,0 +1,185 @@
|
||||
import netaddr
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.models import Interface
|
||||
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object
|
||||
from .constants import *
|
||||
|
||||
|
||||
class PhoneManager(models.Manager):
|
||||
|
||||
def search(self):
|
||||
return Phone.objects.all()
|
||||
|
||||
|
||||
class Phone(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
phone_number = models.CharField(
|
||||
max_length=25,
|
||||
help_text='Phone number 555-555-5555'
|
||||
)
|
||||
ipphonepartition = models.ForeignKey(
|
||||
to='ipphone.IPPhonePartition',
|
||||
on_delete=models.PROTECT,
|
||||
# related_name='',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name='IP Phone Partition'
|
||||
)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
choices=PHONE_STATUS_CHOICES,
|
||||
default=PHONE_STATUS_ACTIVE,
|
||||
verbose_name='Status',
|
||||
help_text='The operational status of this Phone Number'
|
||||
)
|
||||
interface = models.ForeignKey(
|
||||
to='dcim.Interface',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='phone',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
custom_field_values = GenericRelation(
|
||||
to='extras.CustomFieldValue',
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
objects = PhoneManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'phone_number', 'ipphonepartition', 'status', 'device', 'interface_name', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['id', 'phone_number', 'ipphonepartition']
|
||||
verbose_name = 'Phone Number'
|
||||
verbose_name_plural = 'Phone Numbers'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.phone_number)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipphone:phone', args=[self.pk])
|
||||
|
||||
def get_duplicates(self):
|
||||
return Phone.objects.filter(phone_number=self.phone_number).exclude(pk=self.pk)
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.phone_number:
|
||||
|
||||
# Enforce unique Phone (if applicable)
|
||||
if self.ipphonepartition and self.ipphonepartition.enforce_unique:
|
||||
duplicate_pns = self.get_duplicates()
|
||||
if duplicate_pns:
|
||||
raise ValidationError({
|
||||
'phone_number': "Duplicate Phone Number found in {}: {}".format(
|
||||
"IP Phone Partition {}".format(self.ipphonepartition) if self.ipphonepartition else "global table",
|
||||
duplicate_pns.first(),
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Annotate the assigned Interface (if any)
|
||||
try:
|
||||
parent_obj = self.interface
|
||||
except ObjectDoesNotExist:
|
||||
parent_obj = None
|
||||
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
related_object=parent_obj,
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.phone_number,
|
||||
self.get_status_display(),
|
||||
self.device.identifier if self.device else None,
|
||||
self.interface.name if self.interface else None,
|
||||
self.description,
|
||||
self.ipphonepartition,
|
||||
)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
if self.interface:
|
||||
return self.interface.device
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
|
||||
class IPPhonePartitionManager(models.Manager):
|
||||
|
||||
def search(self):
|
||||
return IPPhonePartition.objects.all()
|
||||
|
||||
|
||||
class IPPhonePartition(ChangeLoggedModel, CustomFieldModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
enforce_unique = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name='Enforce unique space',
|
||||
help_text='Prevent duplicate extensions within this IP Phone Partition'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
custom_field_values = GenericRelation(
|
||||
to='extras.CustomFieldValue',
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['name', 'enforce_unique', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = 'IP Phone Partition'
|
||||
verbose_name_plural = 'IP Phone Partitions'
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super().__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipphone:ipphonepartitions', args=[self.pk])
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
self.enforce_unique,
|
||||
self.description,
|
||||
)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self.name
|
108
netbox/ipphone/tables.py
Normal file
108
netbox/ipphone/tables.py
Normal file
@ -0,0 +1,108 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Interface
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
|
||||
from .models import Phone, IPPhonePartition
|
||||
|
||||
IPPHONEPARTITION_LINK = """
|
||||
{% if record.ipphonepartition %}
|
||||
<a href="{{ record.ipphonepartition.get_absolute_url }}">{{ record.ipphonepartition }}</a>
|
||||
{% else %}
|
||||
Global
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
PHONE_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.phone_number }}</a>
|
||||
{% elif perms.ipphone.add_phone %}
|
||||
<a href="{% url 'ipphone:phone_add' %}?phone_number={{ record.1 }}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
|
||||
{% else %}
|
||||
{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} PN{{ record.0|pluralize }} available
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
PHONE_ASSIGN_LINK = """
|
||||
<a href="{% url 'ipphone:phone_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
||||
"""
|
||||
|
||||
PHONE_PARENT = """
|
||||
{% if record.interface %}
|
||||
<a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
{% if record.pk %}
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-success">Available</span>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
#
|
||||
# Phones
|
||||
#
|
||||
|
||||
class PhoneTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
phone_number = tables.TemplateColumn(PHONE_LINK, verbose_name='Phone Number')
|
||||
ipphonepartition = tables.TemplateColumn(IPPHONEPARTITION_LINK, verbose_name='IP Phone Partition')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
parent = tables.TemplateColumn(PHONE_PARENT, orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Phone
|
||||
fields = (
|
||||
'pk', 'phone_number', 'ipphonepartition', 'status', 'parent', 'interface', 'description',
|
||||
)
|
||||
|
||||
|
||||
class PhoneDetailTable(PhoneTable):
|
||||
class Meta(PhoneTable.Meta):
|
||||
fields = (
|
||||
'pk', 'phone_number', 'ipphonepartition', 'status', 'parent', 'interface', 'description',
|
||||
)
|
||||
|
||||
|
||||
class PhoneAssignTable(BaseTable):
|
||||
phone_number = tables.TemplateColumn(PHONE_ASSIGN_LINK, verbose_name='Phone Number')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
parent = tables.TemplateColumn(PHONE_PARENT, orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Phone
|
||||
fields = ('phone_number', 'ipphonepartition', 'status', 'parent', 'interface', 'description')
|
||||
orderable = False
|
||||
|
||||
|
||||
class InterfacePhoneTable(BaseTable):
|
||||
"""
|
||||
List Phone Number assigned to a specific Interface.
|
||||
"""
|
||||
phone_number = tables.TemplateColumn(PHONE_ASSIGN_LINK, verbose_name='Phone Number')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Phone
|
||||
fields = ('phone_number', 'ipphonepartition', 'status', 'description')
|
||||
|
||||
|
||||
#
|
||||
# IPPhonePartitions
|
||||
#
|
||||
|
||||
class IPPhonePartitionTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPPhonePartition
|
||||
fields = ('pk', 'name', 'description', 'enforce_unique')
|
||||
|
3
netbox/ipphone/tests.py
Normal file
3
netbox/ipphone/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
34
netbox/ipphone/urls.py
Normal file
34
netbox/ipphone/urls.py
Normal file
@ -0,0 +1,34 @@
|
||||
from django.urls import path
|
||||
|
||||
from extras.views import ObjectChangeLogView
|
||||
from . import views
|
||||
from .models import Phone, IPPhonePartition
|
||||
|
||||
app_name = 'ipphone'
|
||||
urlpatterns = [
|
||||
|
||||
# Phones
|
||||
path(r'phones/', views.PhoneListView.as_view(), name='phone_list'),
|
||||
path(r'phones/add/', views.PhoneCreateView.as_view(), name='phone_add'),
|
||||
path(r'phones/bulk-add/', views.PhoneBulkCreateView.as_view(), name='phone_bulk_add'),
|
||||
path(r'phones/import/', views.PhoneBulkImportView.as_view(), name='phone_import'),
|
||||
path(r'phones/edit/', views.PhoneBulkEditView.as_view(), name='phone_bulk_edit'),
|
||||
path(r'phones/delete/', views.PhoneBulkDeleteView.as_view(), name='phone_bulk_delete'),
|
||||
path(r'phones/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='phone_changelog', kwargs={'model': Phone}),
|
||||
path(r'phones/assign/', views.PhoneAssignView.as_view(), name='phone_assign'),
|
||||
path(r'phones/<int:pk>/', views.PhoneView.as_view(), name='phone'),
|
||||
path(r'phones/<int:pk>/edit/', views.PhoneEditView.as_view(), name='phone_edit'),
|
||||
path(r'phones/<int:pk>/delete/', views.PhoneDeleteView.as_view(), name='phone_delete'),
|
||||
|
||||
# IPPhonePartitions
|
||||
|
||||
path(r'ipphonepartitions/', views.IPPhonePartitionListView.as_view(), name='ipphonepartition_list'),
|
||||
path(r'ipphonepartitions/add/', views.IPPhonePartitionCreateView.as_view(), name='ipphonepartition_add'),
|
||||
path(r'ipphonepartitions/import/', views.IPPhonePartitionBulkImportView.as_view(), name='ipphonepartition_import'),
|
||||
path(r'ipphonepartitions/edit/', views.IPPhonePartitionBulkEditView.as_view(), name='ipphonepartition_bulk_edit'),
|
||||
path(r'ipphonepartitions/delete/', views.IPPhonePartitionBulkDeleteView.as_view(), name='ipphonepartition_bulk_delete'),
|
||||
path(r'ipphonepartitions/<int:pk>/', views.IPPhonePartitionView.as_view(), name='ipphonepartitions'),
|
||||
path(r'ipphonepartitions/<int:pk>/edit/', views.IPPhonePartitionEditView.as_view(), name='ipphonepartition_edit'),
|
||||
path(r'ipphonepartitions/<int:pk>/delete/', views.IPPhonePartitionDeleteView.as_view(), name='ipphonepartition_delete'),
|
||||
path(r'ipphonepartitions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ipphonepartition_changelog', kwargs={'model': IPPhonePartition}),
|
||||
]
|
244
netbox/ipphone/views.py
Normal file
244
netbox/ipphone/views.py
Normal file
@ -0,0 +1,244 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.views.generic import View
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from dcim.models import Device, Interface
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
from .constants import *
|
||||
from .models import Phone, IPPhonePartition
|
||||
|
||||
|
||||
#
|
||||
# IPPhonePartitions
|
||||
#
|
||||
|
||||
class IPPhonePartitionListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'ipphone.view_ipphonepartition'
|
||||
queryset = IPPhonePartition.objects.all() # prefetch_related('tenant')
|
||||
filter = filters.IPPhonePartitionFilter
|
||||
filter_form = forms.IPPhonePartitionFilterForm
|
||||
table = tables.IPPhonePartitionTable
|
||||
template_name = 'ipphone/ipphonepartition_list.html'
|
||||
|
||||
|
||||
class IPPhonePartitionView(PermissionRequiredMixin, View):
|
||||
permission_required = 'ipphone.view_ipphonepartition'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
ipphonepartition = get_object_or_404(IPPhonePartition.objects.all(), pk=pk)
|
||||
|
||||
return render(request, 'ipphone/ipphonepartition.html', {
|
||||
'ipphonepartition': ipphonepartition
|
||||
})
|
||||
|
||||
|
||||
class IPPhonePartitionCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipphone.add_ipphonepartition'
|
||||
model = IPPhonePartition
|
||||
model_form = forms.IPPhonePartitionForm
|
||||
template_name = 'ipphone/ipphonepartition_edit.html'
|
||||
default_return_url = 'ipphone:ipphonepartition_list'
|
||||
|
||||
|
||||
class IPPhonePartitionEditView(IPPhonePartitionCreateView):
|
||||
permission_required = 'ipphone.change_ipphonepartition'
|
||||
|
||||
|
||||
class IPPhonePartitionDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipphone.delete_ipphonepartition'
|
||||
model = IPPhonePartition
|
||||
default_return_url = 'ipphone:ipphonepartition_list'
|
||||
|
||||
|
||||
class IPPhonePartitionBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipphone.add_ipphonepartition'
|
||||
model_form = forms.IPPhonePartitionCSVForm
|
||||
table = tables.IPPhonePartitionTable
|
||||
default_return_url = 'ipphone:ipphonepartition_list'
|
||||
|
||||
|
||||
class IPPhonePartitionBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipphone.change_ipphonepartition'
|
||||
filter = filters.IPPhonePartitionFilter
|
||||
table = tables.IPPhonePartitionTable
|
||||
form = forms.IPPhonePartitionBulkEditForm
|
||||
default_return_url = 'ipphone:ipphonepartition_list'
|
||||
|
||||
|
||||
class IPPhonePartitionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipphone.delete_ipphonepartition'
|
||||
queryset = IPPhonePartition.objects.all() # prefetch_related('')
|
||||
filter = filters.IPPhonePartitionFilter
|
||||
table = tables.IPPhonePartitionTable
|
||||
default_return_url = 'ipphone:ipphonepartition_list'
|
||||
|
||||
|
||||
#
|
||||
# Phone Numbers
|
||||
#
|
||||
|
||||
class PhoneListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'ipphone.view_phone_number'
|
||||
queryset = Phone.objects.prefetch_related(
|
||||
'interface__device'
|
||||
)
|
||||
filter = filters.PhoneFilter
|
||||
filter_form = forms.PhoneForm
|
||||
table = tables.PhoneDetailTable
|
||||
template_name = 'ipphone/phone_list.html'
|
||||
|
||||
|
||||
class PhoneView(PermissionRequiredMixin, View):
|
||||
permission_required = 'ipphone.view_phone'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
phone = get_object_or_404(Phone.objects.prefetch_related('interface__device'), pk=pk)
|
||||
|
||||
# # TBD
|
||||
|
||||
# # Duplicate Phone Numbers table
|
||||
# # duplicate_pns = Phone.objects.filter(
|
||||
# # phone_number=str(phone.phone_number)
|
||||
# # ).exclude(
|
||||
# # pk=phone.pk
|
||||
# # ).prefetch_related(
|
||||
# # 'interface__device'
|
||||
# # )
|
||||
# duplicate_pns = []
|
||||
|
||||
# duplicate_pns_table = tables.PhoneTable(list(duplicate_pns), orderable=False)
|
||||
|
||||
# related_pns = []
|
||||
|
||||
# # related_pns = Phone.objects.prefetch_related(
|
||||
# # 'interface__device'
|
||||
# # ).exclude(
|
||||
# # phone_number=str(phone.phone_number)
|
||||
# # )
|
||||
|
||||
# related_pns_table = tables.PhoneTable(list(related_pns), orderable=False)
|
||||
|
||||
return render(request, 'ipphone/phone.html', {
|
||||
'phone': phone,
|
||||
# 'duplicate_pns_table': duplicate_pns_table,
|
||||
# 'related_pns_table': related_pns_table,
|
||||
})
|
||||
|
||||
|
||||
class PhoneCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipphone.add_phone'
|
||||
model = Phone
|
||||
model_form = forms.PhoneForm
|
||||
template_name = 'ipphone/phone_edit.html'
|
||||
default_return_url = 'ipphone:phone_list'
|
||||
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
|
||||
interface_id = request.GET.get('interface')
|
||||
if interface_id:
|
||||
try:
|
||||
obj.interface = Interface.objects.get(pk=interface_id)
|
||||
except (ValueError, Interface.DoesNotExist):
|
||||
pass
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
class PhoneEditView(PhoneCreateView):
|
||||
permission_required = 'ipphone.change_phone'
|
||||
|
||||
|
||||
class PhoneAssignView(PermissionRequiredMixin, View):
|
||||
"""
|
||||
Search for Phone Numbers to be assigned to an Interface.
|
||||
"""
|
||||
permission_required = 'ipphone.change_phone'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
# Redirect user if an interface has not been provided
|
||||
if 'interface' not in request.GET:
|
||||
return redirect('ipphone:phone_add')
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request):
|
||||
|
||||
form = forms.PhoneAssignForm()
|
||||
|
||||
return render(request, 'ipphone/phone_assign.html', {
|
||||
'form': form,
|
||||
'return_url': request.GET.get('return_url', ''),
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
|
||||
form = forms.PhoneAssignForm(request.POST)
|
||||
table = None
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
queryset = Phone.objects.prefetch_related(
|
||||
'interface__device'
|
||||
).filter(
|
||||
phone_number__istartswith=form.cleaned_data['phone_number'],
|
||||
)[:100] # Limit to 100 results
|
||||
table = tables.PhoneAssignTable(queryset)
|
||||
|
||||
return render(request, 'ipphone/phone_assign.html', {
|
||||
'form': form,
|
||||
'table': table,
|
||||
'return_url': request.GET.get('return_url', ''),
|
||||
})
|
||||
|
||||
|
||||
class PhoneDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipphone.delete_phone'
|
||||
model = Phone
|
||||
default_return_url = 'ipphone:phone_list'
|
||||
|
||||
|
||||
class PhoneBulkCreateView(PermissionRequiredMixin, BulkCreateView):
|
||||
permission_required = 'ipphone.add_phone'
|
||||
form = forms.PhoneBulkCreateForm
|
||||
model_form = forms.PhoneBulkAddForm
|
||||
pattern_target = 'phone_number'
|
||||
template_name = 'ipphone/phone_bulk_add.html'
|
||||
default_return_url = 'ipphone:phone_list'
|
||||
|
||||
|
||||
class PhoneBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipphone.add_phone'
|
||||
# queryset = Phone.objects.all()
|
||||
model_form = forms.PhoneCSVForm
|
||||
table = tables.PhoneTable
|
||||
default_return_url = 'ipphone:phone_list'
|
||||
|
||||
|
||||
class PhoneBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipphone.change_phone'
|
||||
queryset = Phone.objects.prefetch_related('interface__device')
|
||||
filter = filters.PhoneFilter
|
||||
table = tables.PhoneTable
|
||||
form = forms.PhoneBulkEditForm
|
||||
default_return_url = 'ipphone:phone_list'
|
||||
|
||||
|
||||
class PhoneBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipphone.delete_phone'
|
||||
queryset = Phone.objects.prefetch_related('interface__device')
|
||||
filter = filters.PhoneFilter
|
||||
table = tables.PhoneTable
|
||||
default_return_url = 'ipphone:phone_list'
|
||||
|
||||
|
@ -177,6 +177,7 @@ INSTALLED_APPS = [
|
||||
'circuits',
|
||||
'dcim',
|
||||
'ipam',
|
||||
'ipphone',
|
||||
'extras',
|
||||
'secrets',
|
||||
'tenancy',
|
||||
@ -366,6 +367,7 @@ CACHEOPS = {
|
||||
'auth.permission': {'ops': 'all'},
|
||||
'dcim.*': {'ops': 'all'},
|
||||
'ipam.*': {'ops': 'all'},
|
||||
'ipphone.*': {'ops': 'all'},
|
||||
'extras.*': {'ops': 'all'},
|
||||
'secrets.*': {'ops': 'all'},
|
||||
'users.*': {'ops': 'all'},
|
||||
|
@ -36,6 +36,7 @@ _patterns = [
|
||||
path(r'dcim/', include('dcim.urls')),
|
||||
path(r'extras/', include('extras.urls')),
|
||||
path(r'ipam/', include('ipam.urls')),
|
||||
path(r'ipphone/', include('ipphone.urls')),
|
||||
path(r'secrets/', include('secrets.urls')),
|
||||
path(r'tenancy/', include('tenancy.urls')),
|
||||
path(r'user/', include('users.urls')),
|
||||
@ -47,6 +48,7 @@ _patterns = [
|
||||
path(r'api/dcim/', include('dcim.api.urls')),
|
||||
path(r'api/extras/', include('extras.api.urls')),
|
||||
path(r'api/ipam/', include('ipam.api.urls')),
|
||||
path(r'api/ipphone/', include('ipphone.api.urls')),
|
||||
path(r'api/secrets/', include('secrets.api.urls')),
|
||||
path(r'api/tenancy/', include('tenancy.api.urls')),
|
||||
path(r'api/virtualization/', include('virtualization.api.urls')),
|
||||
|
@ -25,6 +25,9 @@ from extras.models import ObjectChange, ReportResult, TopologyMap
|
||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||
from ipphone.filters import PhoneFilter, IPPhonePartitionFilter
|
||||
from ipphone.models import Phone, IPPhonePartition
|
||||
from ipphone.tables import PhoneTable, IPPhonePartitionTable
|
||||
from secrets.filters import SecretFilter
|
||||
from secrets.models import Secret
|
||||
from secrets.tables import SecretTable
|
||||
@ -204,6 +207,8 @@ class HomeView(View):
|
||||
'aggregate_count': Aggregate.objects.count(),
|
||||
'prefix_count': Prefix.objects.count(),
|
||||
'ipaddress_count': IPAddress.objects.count(),
|
||||
'phone_number_count': Phone.objects.count(),
|
||||
'ipphonepartition_count': IPPhonePartition.objects.count(),
|
||||
'vlan_count': VLAN.objects.count(),
|
||||
|
||||
# Circuits
|
||||
|
@ -553,7 +553,7 @@
|
||||
<strong>Interfaces</strong>
|
||||
<div class="pull-right noprint">
|
||||
<button class="btn btn-default btn-xs toggle-ips" selected="selected">
|
||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs / Phone #s
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -905,8 +905,10 @@ $('button.toggle-ips').click(function() {
|
||||
var selected = $(this).attr('selected');
|
||||
if (selected) {
|
||||
$('#interfaces_table tr.ipaddresses').hide();
|
||||
$('#interfaces_table tr.phones').hide();
|
||||
} else {
|
||||
$('#interfaces_table tr.ipaddresses').show();
|
||||
$('#interfaces_table tr.phones').show();
|
||||
}
|
||||
$(this).attr('selected', !selected);
|
||||
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
||||
|
@ -147,6 +147,11 @@
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipphone.add_phone %}
|
||||
<a href="{% url 'ipphone:phone_add' %}?interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add Phone Number">
|
||||
<i class="glyphicon glyphicon-earphone" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_interface %}
|
||||
{% if iface.cable %}
|
||||
{% include 'dcim/inc/cable_toggle_buttons.html' with cable=iface.cable %}
|
||||
@ -259,3 +264,69 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% with phones=iface.phone.all %}
|
||||
{% if phones %}
|
||||
<tr class="phones">
|
||||
{# Placeholder #}
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{# Phone Numbers table #}
|
||||
<td colspan="9" style="padding: 0">
|
||||
<table class="table table-condensed interface-ips">
|
||||
<thead>
|
||||
<tr class="text-muted">
|
||||
<th class="col-md-3">Phone</th>
|
||||
<th class="col-md-2">Status</th>
|
||||
<th class="col-md-3"></th>
|
||||
<th class="col-md-3">Description</th>
|
||||
<th class="col-md-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for ph in iface.phone.all %}
|
||||
<tr>
|
||||
|
||||
{# Phone Number #}
|
||||
<td>
|
||||
<a href="{% url 'ipphone:phone' pk=ph.pk %}">{{ ph }}</a>
|
||||
</td>
|
||||
|
||||
{# status #}
|
||||
<td>
|
||||
<span class="label label-{{ ph.get_status_class }}">{{ ph.get_status_display }}</span>
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
|
||||
{# Description #}
|
||||
<td>
|
||||
{% if ph.description %}
|
||||
{{ ph.description }}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{# Buttons #}
|
||||
<td class="text-right text-nowrap noprint">
|
||||
{% if perms.ipphone.change_phone %}
|
||||
<a href="{% url 'ipphone:phone_edit' pk=ph.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit Phone Number"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipphone.delete_phone %}
|
||||
<a href="{% url 'ipphone:phone_delete' pk=ph.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete Phone Number"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
@ -203,6 +203,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IP Phone</strong>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
{% if perms.ipphone.view_phones %}
|
||||
<span class="badge pull-right">{{ stats.phone_number_count }}</span>
|
||||
<h4 class="list-group-item-heading"><a href="{% url 'ipphone:phone_list' %}">Phone Numbers</a></h4>
|
||||
{% else %}
|
||||
<span class="badge pull-right"><i class="fa fa-lock"></i></span>
|
||||
<h4 class="list-group-item-heading">Phone Numbers</h4>
|
||||
{% endif %}
|
||||
<p class="list-group-item-text text-muted">Individual Phone Numbers</p>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
{% if perms.ipphone.view_ipphonepartition %}
|
||||
<span class="badge pull-right">{{ stats.ipphonepartition_count }}</span>
|
||||
<h4 class="list-group-item-heading"><a href="{% url 'ipphone:ipphonepartition_list' %}">IP Phone Partitions</a></h4>
|
||||
{% else %}
|
||||
<span class="badge pull-right"><i class="fa fa-lock"></i></span>
|
||||
<h4 class="list-group-item-heading">IP Phone Partitions</h4>
|
||||
{% endif %}
|
||||
<p class="list-group-item-text text-muted">IP Phone Partitions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Circuits</strong>
|
||||
|
@ -292,6 +292,31 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/ipphone/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Phone <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">IP Phone</li>
|
||||
<li{% if not perms.ipphone.view_phone %} class="disabled"{% endif %}>
|
||||
{% if perms.ipphone.add_phone %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'ipphone:phone_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'ipphone:phone_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'ipphone:phone_list' %}">Phone Numbers</a>
|
||||
</li>
|
||||
<li{% if not perms.ipphone.view_ipphonepartition %} class="disabled"{% endif %}>
|
||||
{% if perms.ipphone.add_ipphonepartition %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'ipphone:ipphonepartition_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'ipphone:ipphonepartition_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'ipphone:ipphonepartition_list' %}">IP Phone Partitions</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/virtualization/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Virtualization <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
|
@ -9,7 +9,7 @@
|
||||
<form action="." method="get" class="form">
|
||||
{% for field in filter_form %}
|
||||
<div class="form-group">
|
||||
{% if field.name == "q" %}
|
||||
{% if field.name == "q" or field.name == "phone_number" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
||||
<span class="input-group-btn">
|
||||
|
16
netbox/templates/ipphone/inc/phone_edit_header.html
Normal file
16
netbox/templates/ipphone/inc/phone_edit_header.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% load helpers %}
|
||||
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipphone:phone_add' %}{% querystring request %}">New Number</a>
|
||||
</li>
|
||||
{% if 'interface' in request.GET %}
|
||||
<li role="presentation"{% if active_tab == 'assign' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipphone:phone_assign' %}{% querystring request %}">Assign Number</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li role="presentation"{% if active_tab == 'bulk_add' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipphone:phone_bulk_add' %}{% querystring request %}">Bulk Create</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
87
netbox/templates/ipphone/ipphonepartition.html
Normal file
87
netbox/templates/ipphone/ipphonepartition.html
Normal file
@ -0,0 +1,87 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load custom_links %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipphone:ipphonepartition_list' %}">IP Phone Partitions</a></li>
|
||||
<li>{{ ipphonepartition }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipphone:ipphonepartition_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search IP Phone Partitions" />
|
||||
<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>
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.ipphone.change_ipphonepartition %}
|
||||
<a href="{% url 'ipphone:ipphonepartition_edit' pk=ipphonepartition.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit this IP Phone Partition
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipphone.delete_ipphonepartition %}
|
||||
<a href="{% url 'ipphone:ipphonepartition_delete' pk=ipphonepartition.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
Delete this IP Phone Partition
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}IP Phone Partition {{ ipphonepartition }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=ipphonepartition %}
|
||||
<div class="pull-right noprint">
|
||||
{% custom_links ipphonepartition %}
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ ipphonepartition.get_absolute_url }}">IP Phone Partition</a>
|
||||
</li>
|
||||
{% if perms.extras.view_objectchange %}
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipphone:ipphonepartition_changelog' pk=ipphonepartition.pk %}">Changelog</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IP Phone Partition</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Unique Extension Space</td>
|
||||
<td>
|
||||
{% if ipphonepartition.enforce_unique %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ ipphonepartition.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'extras/inc/tags_panel.html' with tags=ipphonepartition.tags.all url='ipphone:ipphonepartition_list' %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% include 'inc/custom_fields_panel.html' with obj=ipphonepartition %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
27
netbox/templates/ipphone/ipphonepartition_edit.html
Normal file
27
netbox/templates/ipphone/ipphonepartition_edit.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>IP Partition</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.enforce_unique %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tags</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
22
netbox/templates/ipphone/ipphonepartition_list.html
Normal file
22
netbox/templates/ipphone/ipphonepartition_list.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.ipphone.add_ipphonepartition %}
|
||||
{% add_button 'ipphone:ipphonepartition_add' %}
|
||||
{% import_button 'ipphone:ipphonepartition_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Ip Phone Partitions{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipphone:ipphonepartition_bulk_edit' bulk_delete_url='ipphone:ipphonepartition_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
101
netbox/templates/ipphone/phone.html
Normal file
101
netbox/templates/ipphone/phone.html
Normal file
@ -0,0 +1,101 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load custom_links %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row noprint">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipphone:phone_list' %}">Phone Numbers</a></li>
|
||||
<li>{{ phone }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipphone:phone_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search Numbers" />
|
||||
<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>
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.ipphone.change_phone %}
|
||||
<a href="{% url 'ipphone:phone_edit' pk=phone.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit this Number
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipphone.delete_phone %}
|
||||
<a href="{% url 'ipphone:phone_delete' pk=phone.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
Delete this Number
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{% block title %}{{ phone }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=phone %}
|
||||
<div class="pull-right noprint">
|
||||
{% custom_links phone %}
|
||||
</div>
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{{ phone.get_absolute_url }}">Phone Number</a>
|
||||
</li>
|
||||
{% if perms.extras.view_objectchange %}
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipphone:phone_changelog' pk=phone.pk %}">Changelog</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Phone Number</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<span class="label label-{{ phone.get_status_class }}">{{ phone.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Partition</td>
|
||||
<td>
|
||||
{% if phone.ipphonepartition %}
|
||||
<span><a href="{{ phone.ipphonepartition.parent.get_absolute_url }}">{{ phone.ipphonepartition.parent }}</a> ({{ phone.ipphonepartition }})</span>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ phone.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Assignment</td>
|
||||
<td>
|
||||
{% if phone.interface %}
|
||||
<span><a href="{{ phone.interface.parent.get_absolute_url }}">{{ phone.interface.parent }}</a> ({{ phone.interface }})</span>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/custom_fields_panel.html' with obj=phone %}
|
||||
{% include 'extras/inc/tags_panel.html' with tags=phone.tags.all url='ipphone:phone_list' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
47
netbox/templates/ipphone/phone_assign.html
Normal file
47
netbox/templates/ipphone/phone_assign.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load static %}
|
||||
{% load form_helpers %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{% querystring request %}" method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h3>Assign a Phone Number</h3>
|
||||
{% include 'ipphone/inc/phone_edit_header.html' with active_tab='assign' %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Select Phone Number</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.phone_number %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3 text-right">
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% if table %}
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-top: 20px">
|
||||
<h3>Search Results</h3>
|
||||
{% include 'utilities/obj_table.html' with table_template='panel_table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
36
netbox/templates/ipphone/phone_bulk_add.html
Normal file
36
netbox/templates/ipphone/phone_bulk_add.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load static %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Bulk Add Phone Numbers{% endblock %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'ipphone/inc/phone_edit_header.html' with active_tab='bulk_add' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Phone Numbers</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.pattern %}
|
||||
{% render_field model_form.status %}
|
||||
{% render_field model_form.ipphonepartition %}
|
||||
{% render_field model_form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field model_form.tenant_group %}
|
||||
{% render_field model_form.tenant %}
|
||||
</div>
|
||||
</div> -->
|
||||
{% if model_form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields model_form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
64
netbox/templates/ipphone/phone_edit.html
Normal file
64
netbox/templates/ipphone/phone_edit.html
Normal file
@ -0,0 +1,64 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load static %}
|
||||
{% load form_helpers %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block tabs %}
|
||||
{% if not obj.pk %}
|
||||
{% include 'ipphone/inc/phone_edit_header.html' with active_tab='add' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Phone Number</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.phone_number %}
|
||||
{% render_field form.ipphonepartition %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Site</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- {% if obj.interface %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface Assignment</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">{{ obj.interface.parent|model_name|bettertitle }}</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<a href="{{ obj.interface.parent.get_absolute_url }}">{{ obj.interface.parent }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tags</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
|
||||
{% endblock %}
|
22
netbox/templates/ipphone/phone_list.html
Normal file
22
netbox/templates/ipphone/phone_list.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right noprint">
|
||||
{% if perms.ipphone.add_phone %}
|
||||
{% add_button 'ipphone:phone_add' %}
|
||||
{% import_button 'ipphone:phone_import' %}
|
||||
{% endif %}
|
||||
{% export_button content_type %}
|
||||
</div>
|
||||
<h1>{% block title %}Phone Numbers{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipphone:phone_bulk_edit' bulk_delete_url='ipphone:phone_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3 noprint">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/tags_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -677,7 +677,7 @@ class ChainedFieldsMixin(forms.BaseForm):
|
||||
field.queryset = field.queryset.filter(**filters_dict)
|
||||
elif not self.is_bound and getattr(self, 'instance', None) and hasattr(self.instance, field_name):
|
||||
obj = getattr(self.instance, field_name)
|
||||
if obj is not None:
|
||||
if obj is not None and obj != '':
|
||||
field.queryset = field.queryset.filter(pk=obj.pk)
|
||||
else:
|
||||
field.queryset = field.queryset.none()
|
||||
|
@ -131,6 +131,7 @@ def example_choices(field, arg=3):
|
||||
Returns a number (default: 3) of example choices for a ChoiceFiled (useful for CSV import forms).
|
||||
"""
|
||||
examples = []
|
||||
print(field)
|
||||
if hasattr(field, 'queryset'):
|
||||
choices = [
|
||||
(obj.pk, getattr(obj, field.to_field_name)) for obj in field.queryset[:arg + 1]
|
||||
|
Loading…
Reference in New Issue
Block a user