This commit is contained in:
Nathan Gotz 2016-09-15 16:39:54 +00:00 committed by GitHub
commit 290b5ff96e
29 changed files with 554 additions and 184 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@ configuration.py
!upgrade.sh !upgrade.sh
fabfile.py fabfile.py
*.swp *.swp
gunicorn_config.py
netbox/static/

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Provider, CircuitType, Circuit from .models import Provider, CircuitType, Circuit, Termination
@admin.register(Provider) @admin.register(Provider)
@ -21,11 +21,21 @@ class CircuitTypeAdmin(admin.ModelAdmin):
@admin.register(Circuit) @admin.register(Circuit)
class CircuitAdmin(admin.ModelAdmin): class CircuitAdmin(admin.ModelAdmin):
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human', list_display = ['cid', 'provider', 'type', 'tenant', 'install_date']
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
list_filter = ['provider', 'type', 'tenant'] list_filter = ['provider', 'type', 'tenant']
exclude = ['interface']
def get_queryset(self, request): def get_queryset(self, request):
qs = super(CircuitAdmin, self).get_queryset(request) qs = super(CircuitAdmin, self).get_queryset(request)
return qs.select_related('provider', 'type', 'tenant', 'site') return qs.select_related('provider', 'type', 'tenant')
@admin.register(Termination)
class TerminationAdmin(admin.ModelAdmin):
list_display = ['tid', 'circuit', 'site', 'port_speed_human',
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
list_filter = ['site', 'tid', 'circuit']
exclude = ['interface']
def get_queryset(self, request):
qs = super(TerminationAdmin, self).get_queryset(request)
return qs.select_related('site', 'circuit')

View File

@ -49,13 +49,10 @@ class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
provider = ProviderNestedSerializer() provider = ProviderNestedSerializer()
type = CircuitTypeNestedSerializer() type = CircuitTypeNestedSerializer()
tenant = TenantNestedSerializer() tenant = TenantNestedSerializer()
site = SiteNestedSerializer()
interface = InterfaceNestedSerializer()
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed', fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'custom_fields']
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields']
class CircuitNestedSerializer(CircuitSerializer): class CircuitNestedSerializer(CircuitSerializer):

View File

@ -43,7 +43,7 @@ class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
""" """
List circuits (filterable) List circuits (filterable)
""" """
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\ queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
.prefetch_related('custom_field_values__field') .prefetch_related('custom_field_values__field')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer
filter_class = CircuitFilter filter_class = CircuitFilter
@ -53,6 +53,6 @@ class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
""" """
Retrieve a single circuit Retrieve a single circuit
""" """
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\ queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
.prefetch_related('custom_field_values__field') .prefetch_related('custom_field_values__field')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer

View File

@ -14,12 +14,12 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='circuits__site', name='circuits__terminations__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site', label='Site',
) )
site = django_filters.ModelMultipleChoiceFilter( site = django_filters.ModelMultipleChoiceFilter(
name='circuits__site', name='circuits__terminations__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
@ -75,26 +75,13 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date'] fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'install_date']
def search(self, queryset, value): def search(self, queryset, value):
return queryset.filter( return queryset.filter(
Q(cid__icontains=value) | Q(cid__icontains=value) |
Q(xconnect_id__icontains=value) |
Q(pp_info__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )

View File

@ -1,4 +1,5 @@
from django import forms from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
@ -9,7 +10,7 @@ from utilities.forms import (
SlugField, get_filter_choices, SlugField, get_filter_choices,
) )
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider, Termination
# #
@ -57,6 +58,11 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
comments = CommentField() comments = CommentField()
def provider_site_choices():
site_choices = Site.objects.all()
return [(s.slug, s.name) for s in site_choices]
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm): class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider model = Provider
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug')) site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug'))
@ -97,8 +103,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
class Meta: class Meta:
model = Circuit model = Circuit
fields = [ fields = [
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date', 'cid', 'type', 'provider', 'tenant', 'install_date', 'comments'
'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
] ]
help_texts = { help_texts = {
'cid': "Unique circuit ID", 'cid': "Unique circuit ID",
@ -165,8 +170,7 @@ class CircuitFromCSVForm(forms.ModelForm):
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed', fields = ['cid', 'provider', 'type', 'tenant', 'install_date']
'commit_rate', 'xconnect_id', 'pp_info']
class CircuitImportForm(BulkImportForm, BootstrapMixin): class CircuitImportForm(BulkImportForm, BootstrapMixin):
@ -188,4 +192,108 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
type = FilterChoiceField(choices=get_filter_choices(CircuitType, id_field='slug', count_field='circuits')) type = FilterChoiceField(choices=get_filter_choices(CircuitType, id_field='slug', count_field='circuits'))
provider = FilterChoiceField(choices=get_filter_choices(Provider, id_field='slug', count_field='circuits')) provider = FilterChoiceField(choices=get_filter_choices(Provider, id_field='slug', count_field='circuits'))
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='circuits')) tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='circuits'))
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='circuits'))
#
# Terminations
#
class TerminationForm(BootstrapMixin, CustomFieldForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
rack = forms.ModelChoiceField(
queryset=Rack.objects.all(),
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device'}
)
)
device = forms.ModelChoiceField(
queryset=Device.objects.all(),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?rack_id={{rack}}',
attrs={'filter-for': 'interface'}
)
)
livesearch = forms.CharField(
required=False,
label='Device',
widget=Livesearch(
query_key='q',
query_url='dcim-api:device_list',
field_to_update='device'
)
)
interface = forms.ModelChoiceField(
queryset=Interface.objects.all(),
required=False,
label='Interface',
widget=APISelect(
api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
disabled_indicator='is_connected'
)
)
comments = CommentField()
class Meta:
model = Termination
fields = [
'tid', 'site', 'rack', 'device', 'livesearch',
'interface', 'port_speed', 'upstream_speed', 'commit_rate',
'xconnect_id', 'pp_info', 'comments'
]
help_texts = {
'tid': "Termination ID",
'port_speed': "Physical circuit speed",
'commit_rate': "Commited rate",
'xconnect_id': "ID of the local cross-connect",
'pp_info': "Patch panel ID and port number(s)"
}
widgets = {
'circuit': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super(TerminationForm, self).__init__(*args, **kwargs)
# If this circuit has been assigned to an interface, initialize rack and device
if self.instance.interface:
self.initial['rack'] = self.instance.interface.device.rack
self.initial['device'] = self.instance.interface.device
# Limit rack choices
if self.is_bound:
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
else:
self.fields['rack'].choices = []
# Limit device choices
if self.is_bound and self.data.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
elif self.initial.get('rack'):
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
else:
self.fields['device'].choices = []
# Limit interface choices
if self.is_bound and self.data.get('device'):
interfaces = Interface.objects.filter(device=self.data['device'])\
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
elif self.initial.get('device'):
interfaces = Interface.objects.filter(device=self.initial['device'])\
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
else:
interfaces = []
self.fields['interface'].choices = [
(iface.id, {
'label': iface.name,
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
}) for iface in interfaces
]

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-08 02:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0018_device_add_asset_tag'),
('circuits', '0005_circuit_add_upstream_speed'),
]
operations = [
migrations.CreateModel(
name='Termination',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('tid', models.CharField(max_length=50, verbose_name=b'Termination ID')),
('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')),
('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')),
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (Kbps)')),
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')),
('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')),
('comments', models.TextField(blank=True)),
('circuit', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='terminations', to='circuits.Circuit')),
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='termination', to='dcim.Interface')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='terminations', to='dcim.Site')),
],
options={
'ordering': ['circuit', 'tid'],
},
),
migrations.AlterUniqueTogether(
name='termination',
unique_together=set([('circuit', 'tid')]),
),
]

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-08 02:20
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('circuits', '0006_auto_20160908_0213'),
]
operations = [
migrations.RemoveField(
model_name='circuit',
name='commit_rate',
),
migrations.RemoveField(
model_name='circuit',
name='interface',
),
migrations.RemoveField(
model_name='circuit',
name='port_speed',
),
migrations.RemoveField(
model_name='circuit',
name='pp_info',
),
migrations.RemoveField(
model_name='circuit',
name='site',
),
migrations.RemoveField(
model_name='circuit',
name='upstream_speed',
),
migrations.RemoveField(
model_name='circuit',
name='xconnect_id',
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-11 12:29
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('circuits', '0007_auto_20160908_0220'),
]
operations = [
migrations.RemoveField(
model_name='termination',
name='comments',
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-11 12:59
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0008_remove_termination_comments'),
]
operations = [
migrations.AddField(
model_name='termination',
name='comments',
field=models.TextField(blank=True),
),
]

View File

@ -1,15 +1,13 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import Site, Interface from dcim.models import Site, Interface
from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
class Provider(CreatedUpdatedModel, CustomFieldModel): class Provider(CreatedUpdatedModel):
""" """
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider. stores information pertinent to the user's relationship with the Provider.
@ -22,7 +20,6 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
noc_contact = models.TextField(blank=True, verbose_name='NOC contact') noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
admin_contact = models.TextField(blank=True, verbose_name='Admin contact') admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -61,7 +58,7 @@ class CircuitType(models.Model):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug)
class Circuit(CreatedUpdatedModel, CustomFieldModel): class Circuit(CreatedUpdatedModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device
@ -71,17 +68,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT) provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT) type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT) tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed') install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
help_text='Upstream speed, if different from port speed')
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
class Meta: class Meta:
ordering = ['provider', 'cid'] ordering = ['provider', 'cid']
@ -95,6 +83,43 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
def to_csv(self): def to_csv(self):
return ','.join([ return ','.join([
self.cid,
self.provider.name,
self.type.name,
self.tenant.name if self.tenant else '',
self.install_date.isoformat() if self.install_date else '',
])
class Termination(CreatedUpdatedModel):
"""
A Termination is where a site
"""
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.PROTECT)
tid = models.CharField(max_length=50, verbose_name='Termination ID')
site = models.ForeignKey(Site, related_name='terminations', on_delete=models.PROTECT)
interface = models.OneToOneField(Interface, related_name='termination', blank=True, null=True)
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
help_text='Upstream speed, if different from port speed')
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
comments = models.TextField(blank=True)
class Meta:
ordering = ['circuit', 'tid']
unique_together = ['circuit', 'tid']
def __unicode__(self):
return u'{} Termination {}'.format(self.circuit.cid, self.tid)
def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.circuit.pk])
def to_csv(self):
return ','.join([
self.tid,
self.cid, self.cid,
self.provider.name, self.provider.name,
self.type.name, self.type.name,

View File

@ -56,12 +56,9 @@ class CircuitTable(BaseTable):
type = tables.Column(verbose_name='Type') type = tables.Column(verbose_name='Type')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider') provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') termination_count = tables.Column(accessor=Accessor('count_terminations'),
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'), verbose_name='Terminations')
verbose_name='Port Speed')
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
verbose_name='Commit Rate')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Circuit model = Circuit
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate') fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'termination_count')

View File

@ -31,4 +31,9 @@ urlpatterns = [
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'), url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'), url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
# Terminations
url(r'^circuits/(?P<pk>\d+)/terminations/add/$', views.termination_add, name='termination_add'),
url(r'^terminations/(?P<pk>\d+)/edit/$', views.TerminationEditView.as_view(), name='termination_edit'),
url(r'^terminations/(?P<pk>\d+)/delete/$', views.termination_delete, name='termination_delete'),
] ]

View File

@ -1,14 +1,18 @@
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, redirect, render
from django.contrib.auth.decorators import permission_required
from django.core.urlresolvers import reverse, resolve
from django.contrib import messages
from extras.models import Graph, GRAPH_TYPE_PROVIDER from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.forms import ConfirmationForm
from utilities.views import ( from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider, Termination
# #
@ -27,7 +31,7 @@ class ProviderListView(ObjectListView):
def provider(request, slug): def provider(request, slug):
provider = get_object_or_404(Provider, slug=slug) provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device') circuits = Circuit.objects.filter(provider=provider)
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
return render(request, 'circuits/provider.html', { return render(request, 'circuits/provider.html', {
@ -103,7 +107,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class CircuitListView(ObjectListView): class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site') queryset = Circuit.objects.select_related('provider', 'type', 'tenant').annotate(count_terminations=Count('terminations'))
filter = filters.CircuitFilter filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm filter_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable
@ -155,3 +159,64 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit' permission_required = 'circuits.delete_circuit'
cls = Circuit cls = Circuit
default_redirect_url = 'circuits:circuit_list' default_redirect_url = 'circuits:circuit_list'
#
# Terminations
#
@permission_required('circuits.change_circuit')
def termination_add(request, pk):
circuit = get_object_or_404(Circuit, pk=pk)
if request.method == 'POST':
form = forms.TerminationForm(request.POST)
if form.is_valid():
if not form.errors:
new_termination = form.save(commit=False)
new_termination.circuit = circuit
new_termination.save()
if '_addanother' in request.POST:
return redirect('circuits:termination_add', pk=circuit.pk)
else:
return redirect('circuits:circuit', pk=circuit.pk)
else:
form = forms.TerminationForm()
return render(request, 'circuits/termination_edit.html', {
'form': form,
'obj_type': "Termination",
'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk}),
})
class TerminationEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuit'
model = Termination
form_class = forms.TerminationForm
fields_initial = ['site']
template_name = 'circuits/termination_edit.html'
cancel_url = 'circuits:circuit_list'
@permission_required('circuits.delete_circuit')
def termination_delete(request, pk):
termination = get_object_or_404(Termination, pk=pk)
if request.method == 'POST':
form = ConfirmationForm(request.POST)
if form.is_valid():
termination.delete()
messages.success(request, "Termination {0} has been deleted from {1}".format(termination, termination.circuit))
return redirect('circuits:circuit', pk=termination.circuit.pk)
else:
form = ConfirmationForm()
return render(request, 'circuits/termination_delete.html', {
'termination': termination,
'form': form,
'cancel_url': reverse('circuits:circuit', kwargs={'pk': termination.circuit.pk}),
})

View File

@ -20,7 +20,7 @@ class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
class Meta: class Meta:
model = Site model = Site
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments', fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits'] 'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices']
class SiteNestedSerializer(SiteSerializer): class SiteNestedSerializer(SiteSerializer):

View File

@ -484,7 +484,7 @@ class RelatedConnectionsView(APIView):
# Interface connections # Interface connections
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b', interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
'circuit') 'termination')
for iface in interfaces: for iface in interfaces:
data = serializers.InterfaceDetailSerializer(instance=iface).data data = serializers.InterfaceDetailSerializer(instance=iface).data
del(data['device']) del(data['device'])

View File

@ -1051,7 +1051,7 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
# Initialize interface A choices # Initialize interface A choices
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \ device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
.select_related('circuit', 'connected_as_a', 'connected_as_b') .select_related('termination', 'connected_as_a', 'connected_as_b')
self.fields['interface_a'].choices = [ self.fields['interface_a'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
] ]
@ -1067,10 +1067,10 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
# Initialize interface_b choices if device_b is set # Initialize interface_b choices if device_b is set
if self.is_bound: if self.is_bound:
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \ device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
elif self.initial.get('device_b'): elif self.initial.get('device_b'):
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \ device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b') .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
else: else:
device_b_interfaces = [] device_b_interfaces = []
self.fields['interface_b'].choices = [ self.fields['interface_b'].choices = [

View File

@ -304,7 +304,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
@property @property
def count_circuits(self): def count_circuits(self):
return self.circuits.count() return self.terminations.count()
# #
@ -1087,7 +1087,7 @@ class Interface(models.Model):
@property @property
def is_connected(self): def is_connected(self):
try: try:
return bool(self.circuit) return bool(self.termination)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
return bool(self.connection) return bool(self.connection)

View File

@ -26,7 +26,6 @@ class SiteTest(APITestCase):
'count_vlans', 'count_vlans',
'count_racks', 'count_racks',
'count_devices', 'count_devices',
'count_circuits'
] ]
nested_fields = [ nested_fields = [

View File

@ -15,7 +15,7 @@ from django.utils.http import urlencode
from django.views.generic import View from django.views.generic import View
from ipam.models import Prefix, IPAddress, VLAN from ipam.models import Prefix, IPAddress, VLAN
from circuits.models import Circuit from circuits.models import Circuit, Termination
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.views import ( from utilities.views import (
@ -78,7 +78,7 @@ def site(request, slug):
'device_count': Device.objects.filter(rack__site=site).count(), 'device_count': Device.objects.filter(rack__site=site).count(),
'prefix_count': Prefix.objects.filter(site=site).count(), 'prefix_count': Prefix.objects.filter(site=site).count(),
'vlan_count': VLAN.objects.filter(site=site).count(), 'vlan_count': VLAN.objects.filter(site=site).count(),
'circuit_count': Circuit.objects.filter(site=site).count(), 'termination_count': Termination.objects.filter(site=site).count(),
} }
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
topology_maps = TopologyMap.objects.filter(site=site) topology_maps = TopologyMap.objects.filter(site=site)
@ -554,9 +554,9 @@ def device(request, pk):
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
) )
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\ interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
.select_related('connected_as_a', 'connected_as_b', 'circuit') .select_related('connected_as_a', 'connected_as_b', 'termination')
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\ mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
.select_related('connected_as_a', 'connected_as_b', 'circuit') .select_related('connected_as_a', 'connected_as_b', 'termination')
device_bays = natsorted( device_bays = natsorted(
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
key=attrgetter('name') key=attrgetter('name')

View File

@ -27,6 +27,10 @@
</div> </div>
<div class="pull-right"> <div class="pull-right">
{% if perms.circuits.change_circuit %} {% if perms.circuits.change_circuit %}
<a href="{% url 'circuits:termination_add' pk=circuit.pk %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add termination
</a>
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning"> <a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> <span class="fa fa-pencil" aria-hidden="true"></span>
Edit this circuit Edit this circuit
@ -81,73 +85,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<td>Speed</td>
<td>
{% if circuit.upstream_speed %}
<i class="fa fa-arrow-down" title="Downstream"></i> {{ circuit.port_speed_human }} &nbsp;
<i class="fa fa-arrow-up" title="Upstream"></i> {{ circuit.upstream_speed_human }}
{% else %}
{{ circuit.port_speed_human }}
{% endif %}
</td>
</tr>
<tr>
<td>Commit Rate</td>
<td>
{% if circuit.commit_rate %}
{{ circuit.commit_rate_human }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table>
</div>
{% include 'inc/created_updated.html' with obj=circuit %}
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Termination</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Site</td>
<td>
<a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a>
</td>
</tr>
<tr>
<td>Termination</td>
<td>
{% if circuit.interface %}
<span><a href="{% url 'dcim:device' pk=circuit.interface.device.pk %}">{{ circuit.interface.device }}</a> {{ circuit.interface }}</span>
{% else %}
<span class="text-muted">Not defined</span>
{% endif %}
</td>
</tr>
<tr>
<td>Cross-Connect</td>
<td>
{% if circuit.xconnect_id %}
{{ circuit.xconnect_id }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Patch Panel/Port</td>
<td>
{% if circuit.pp_info %}
{{ circuit.pp_info }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table> </table>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
@ -162,6 +99,41 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% include 'inc/created_updated.html' with obj=circuit %}
</div>
<div class="col-md-6">
{% for t in circuit.terminations.all %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="pull-right">
{% if perms.circuits.change_circuit %}
<a href="{% url 'circuits:termination_edit' pk=t.pk %}" class="btn btn-info btn-xs" title="Edit interface">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.circuits.delete_circuit %}
<a href="{% url 'circuits:termination_delete' pk=t.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}
</span>
<strong>Termination ID {{t.tid }}</strong>
</div>
<table class="table table-hover panel-body">
{% include 'circuits/inc/_termination.html' %}
</table>
</div>
{% empty %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Termination</strong>
</div>
<div class="text-muted" colspan="7">
None
</div>
</div>
{% endfor %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -11,16 +11,6 @@
{% render_field form.type %} {% render_field form.type %}
{% render_field form.tenant %} {% render_field form.tenant %}
{% render_field form.install_date %} {% render_field form.install_date %}
{% render_field form.xconnect_id %}
{% render_field form.pp_info %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Bandwidth</strong></div>
<div class="panel-body">
{% render_field form.port_speed %}
{% render_field form.upstream_speed %}
{% render_field form.commit_rate %}
</div> </div>
</div> </div>
{% if form.custom_fields %} {% if form.custom_fields %}
@ -31,26 +21,6 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Termination</strong></div>
<div class="panel-body">
{% render_field form.site %}
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
<li role="presentation"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="select">
{% render_field form.rack %}
{% render_field form.device %}
</div>
<div class="tab-pane" id="search">
{% render_field form.livesearch %}
</div>
</div>
{% render_field form.interface %}
</div>
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div> <div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -0,0 +1,49 @@
{% load helpers %}
<tr>
<th>Site</th>
<td>
<a href="{% url 'dcim:site' slug=t.site.slug %}">{{ t.site }}</a>
</td>
</tr>
<tr>
<th>Termination</th>
<td>
{% if t.interface %}
<span><a href="{% url 'dcim:device' pk=t.interface.device.pk %}">{{ t.interface.device }}</a> {{ t.interface }}</span>
{% else %}
<span class="text-muted">Not defined</span>
{% endif %}
</td>
</tr>
<tr>
<th>Cross-Connect</th>
<td>
{% if t.xconnect_id %}
{{ t.xconnect_id }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<th>Patch Panel/Port</th>
<td>
{% if t.pp_info %}
{{ t.pp_info }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<th colspan="2">Comments</th>
</tr>
<tr>
{% if t.comments %}
<td colspan="2">
{{ t.comments|gfm }}
</td>
{% else %}
<td class="text-muted" colspan="2">None</td>
{% endif %}
</tr>

View File

@ -133,15 +133,6 @@
<td> <td>
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a> <a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
</td> </td>
<td>
<a href="{% url 'dcim:site' slug=c.site.slug %}">{{ c.site }}</a>
</td>
<td>
{% if c.interface %}
<a href="{% url 'dcim:device' pk=c.interface.device.pk %}">{{ c.interface.device }}</a>
{% endif %}
</td>
<td>{{ c.port_speed_human }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>

View File

@ -0,0 +1,8 @@
{% extends 'utilities/confirmation_form.html' %}
{% load form_helpers %}
{% block title %}Delete termination {{ termination }}?{% endblock %}
{% block message %}
<p>Are you sure you want to delete this termination from <strong>{{ termination.circuit.cid }}</strong>?</p>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends 'utilities/obj_edit.html' %}
{% load static from staticfiles %}
{% load form_helpers %}
{% block form %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Termination</strong></div>
<div class="panel-body">
{{ form.circuit }}
{% render_field form.tid %}
{% render_field form.xconnect_id %}
{% render_field form.pp_info %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Bandwidth</strong></div>
<div class="panel-body">
{% render_field form.port_speed %}
{% render_field form.upstream_speed %}
{% render_field form.commit_rate %}
</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>Device &amp; Interface</strong></div>
<div class="panel-body">
{% render_field form.site %}
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
<li role="presentation"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="select">
{% render_field form.rack %}
{% render_field form.device %}
</div>
<div class="tab-pane" id="search">
{% render_field form.livesearch %}
</div>
</div>
{% render_field form.interface %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Comments</strong></div>
<div class="panel-body">
{% render_field form.comments %}
</div>
</div>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/livesearch.js' %}"></script>
{% endblock %}

View File

@ -24,9 +24,9 @@
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span> <span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
</td> </td>
{% endwith %} {% endwith %}
{% elif iface.circuit %} {% elif iface.termination %}
<td colspan="2"> <td colspan="2">
<a href="{% url 'circuits:circuit' pk=iface.circuit.pk %}">{{ iface.circuit }}</a> <a href="{% url 'circuits:circuit' pk=iface.termination.circuit.pk %}">{{ iface.termination.circuit }}</a>
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td colspan="2">
@ -35,7 +35,7 @@
{% endif %} {% endif %}
<td class="text-right"> <td class="text-right">
{% if show_graphs %} {% if show_graphs %}
{% if iface.circuit or iface.connection %} {% if iface.termination or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i> <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
</button> </button>
@ -56,8 +56,8 @@
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection"> <a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i> <i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
</a> </a>
{% elif iface.circuit and perms.circuits.change_circuit %} {% elif iface.termination and perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=iface.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit"> <a href="{% url 'circuits:circuit_edit' pk=iface.termination.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i> <i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
@ -71,7 +71,7 @@
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.delete_interface %} {% if perms.dcim.delete_interface %}
{% if iface.connection or iface.circuit %} {% if iface.connection or iface.termination.circuit %}
<button class="btn btn-danger btn-xs" disabled="disabled"> <button class="btn btn-danger btn-xs" disabled="disabled">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>

View File

@ -151,8 +151,8 @@
<p>VLANs</p> <p>VLANs</p>
</div> </div>
<div class="col-md-4 text-center"> <div class="col-md-4 text-center">
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2> <h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.termination_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.termination_count }}</a></h2>
<p>Circuits</p> <p>Terminations</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -55,7 +55,6 @@ def get_filter_choices(model, id_field='pk', select_related=[], count_field=None
return [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset] return [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset]
# #
# Widgets # Widgets
# #