1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merged develop

This commit is contained in:
Jeremy Stretch
2017-02-28 16:10:53 -05:00
parent 8f42f59a80
commit c0152940f9
29 changed files with 437 additions and 51 deletions

View File

@ -6,6 +6,10 @@ How you define sites will depend on the nature of your organization, but typical
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment. Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment.
### Regions
Sites can optionally be arranged by geographic region. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy.
--- ---
# Racks # Racks

View File

@ -119,9 +119,17 @@ class CircuitListView(ObjectListView):
def circuit(request, pk): def circuit(request, pk):
circuit = get_object_or_404(Circuit, pk=pk) circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first() termination_a = CircuitTermination.objects.select_related(
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first() 'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
).first()
termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
).first()
return render(request, 'circuits/circuit.html', { return render(request, 'circuits/circuit.html', {
'circuit': circuit, 'circuit': circuit,

View File

@ -1,13 +1,24 @@
from django.contrib import admin from django.contrib import admin
from django.db.models import Count from django.db.models import Count
from mptt.admin import MPTTModelAdmin
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Site, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
Site,
) )
@admin.register(Region)
class RegionAdmin(MPTTModelAdmin):
list_display = ['name', 'parent', 'slug']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(Site) @admin.register(Site)
class SiteAdmin(admin.ModelAdmin): class SiteAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'facility', 'asn'] list_display = ['name', 'slug', 'facility', 'asn']

View File

@ -6,18 +6,46 @@ from dcim.models import (
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface, DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES, RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
) )
from extras.api.serializers import CustomFieldModelSerializer from extras.api.serializers import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer from utilities.api import ChoiceFieldSerializer
#
# Regions
#
class NestedRegionSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
class Meta:
model = Region
fields = ['id', 'url', 'name', 'slug']
class RegionSerializer(serializers.ModelSerializer):
parent = NestedRegionSerializer()
class Meta:
model = Region
fields = ['id', 'url', 'name', 'slug', 'parent']
class WritableRegionSerializer(serializers.ModelSerializer):
class Meta:
model = Region
fields = ['id', 'name', 'slug', 'parent']
# #
# Sites # Sites
# #
class SiteSerializer(CustomFieldModelSerializer): class SiteSerializer(CustomFieldModelSerializer):
region = NestedRegionSerializer()
tenant = NestedTenantSerializer() tenant = NestedTenantSerializer()
class Meta: class Meta:

View File

@ -10,6 +10,7 @@ from . import views
router = routers.DefaultRouter() router = routers.DefaultRouter()
# Sites # Sites
router.register(r'regions', views.RegionViewSet)
router.register(r'sites', views.SiteViewSet) router.register(r'sites', views.SiteViewSet)
# Racks # Racks

View File

@ -13,7 +13,7 @@ from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
RackRole, Site, RackRole, Region, Site,
) )
from dcim import filters from dcim import filters
from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
@ -25,6 +25,16 @@ from .exceptions import MissingFilterException
from . import serializers from . import serializers
#
# Regions
#
class RegionViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer
# #
# Sites # Sites
# #

View File

@ -10,7 +10,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Site, VIRTUAL_IFACE_TYPES, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
) )
@ -19,6 +19,17 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='search', action='search',
label='Search', label='Search',
) )
region_id = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
to_field_name='slug',
label='Region (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant', name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),

View File

@ -1,5 +1,7 @@
import re import re
from mptt.forms import TreeNodeChoiceField
from django import forms from django import forms
from django.contrib.postgres.forms.array import SimpleArrayField from django.contrib.postgres.forms.array import SimpleArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -11,7 +13,7 @@ from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
SmallTextarea, SlugField, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
) )
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
@ -20,7 +22,7 @@ from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
VIRTUAL_IFACE_TYPES VIRTUAL_IFACE_TYPES
) )
@ -63,18 +65,33 @@ class DeviceComponentForm(BootstrapMixin, forms.Form):
super(DeviceComponentForm, self).__init__(*args, **kwargs) super(DeviceComponentForm, self).__init__(*args, **kwargs)
#
# Regions
#
class RegionForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
class Meta:
model = Region
fields = ['parent', 'name', 'slug']
# #
# Sites # Sites
# #
class SiteForm(BootstrapMixin, CustomFieldForm): class SiteForm(BootstrapMixin, CustomFieldForm):
region = TreeNodeChoiceField(queryset=Region.objects.all())
slug = SlugField() slug = SlugField()
comments = CommentField() comments = CommentField()
class Meta: class Meta:
model = Site model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name', fields = [
'contact_phone', 'contact_email', 'comments'] 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
'contact_name', 'contact_phone', 'contact_email', 'comments',
]
widgets = { widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}), 'physical_address': SmallTextarea(attrs={'rows': 3}),
'shipping_address': SmallTextarea(attrs={'rows': 3}), 'shipping_address': SmallTextarea(attrs={'rows': 3}),
@ -89,12 +106,22 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
class SiteFromCSVForm(forms.ModelForm): class SiteFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, region = forms.ModelChoiceField(
error_messages={'invalid_choice': 'Tenant not found.'}) Region.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
}
)
tenant = forms.ModelChoiceField(
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
'invalid_choice': 'Tenant not found.'
}
)
class Meta: class Meta:
model = Site model = Site
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email'] fields = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
]
class SiteImportForm(BootstrapMixin, BulkImportForm): class SiteImportForm(BootstrapMixin, BulkImportForm):
@ -103,18 +130,27 @@ class SiteImportForm(BootstrapMixin, BulkImportForm):
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN') asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
class Meta: class Meta:
nullable_fields = ['tenant', 'asn'] nullable_fields = ['region', 'tenant', 'asn']
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site model = Site
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug', region = FilterTreeNodeMultipleChoiceField(
null_option=(0, 'None')) queryset=Region.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
required=False,
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
null_option=(0, 'None')
)
# #

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-02-28 17:14
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0030_interface_add_lag'),
]
operations = [
migrations.CreateModel(
name='Region',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(unique=True)),
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(db_index=True, editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='site',
name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
),
]

View File

@ -1,5 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from mptt.models import MPTTModel, TreeForeignKey
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -200,6 +202,29 @@ RPC_CLIENT_CHOICES = [
] ]
#
# Regions
#
@python_2_unicode_compatible
class Region(MPTTModel):
"""
Sites can be grouped within geographic Regions.
"""
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
# #
# Sites # Sites
# #
@ -218,7 +243,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT) region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL)
tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT)
facility = models.CharField(max_length=50, blank=True) facility = models.CharField(max_length=50, blank=True)
asn = ASNField(blank=True, null=True, verbose_name='ASN') asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True) physical_address = models.CharField(max_length=200, blank=True)
@ -244,6 +270,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
return csv_format([ return csv_format([
self.name, self.name,
self.slug, self.slug,
self.region.name if self.region else None,
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.facility, self.facility,
self.asn, self.asn,
@ -1248,8 +1275,8 @@ class Interface(models.Model):
) )
}) })
# A LAG interface cannot have a parent LAG # A virtual interface cannot have a parent LAG
if self.form_factor == IFACE_FF_LAG and self.lag is not None: if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
raise ValidationError({ raise ValidationError({
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) 'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
}) })

View File

@ -6,10 +6,28 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, Site, RackGroup, Region, Site,
) )
REGION_LINK = """
{% if record.get_children %}
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i></a>
{% else %}
<span style="padding-left: {{ record.get_ancestors|length }}9px">
{% endif %}
{{ record.name }}
</span>
"""
SITE_REGION_LINK = """
{% if record.region %}
<a href="{% url 'dcim:site_list' %}?region={{ record.region.slug }}">{{ record.region }}</a>
{% else %}
&mdash;
{% endif %}
"""
COLOR_LABEL = """ COLOR_LABEL = """
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label> <label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
""" """
@ -20,6 +38,12 @@ DEVICE_LINK = """
</a> </a>
""" """
REGION_ACTIONS = """
{% if perms.dcim.change_region %}
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
RACKGROUP_ACTIONS = """ RACKGROUP_ACTIONS = """
{% if perms.dcim.change_rackgroup %} {% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a> <a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
@ -76,6 +100,27 @@ UTILIZATION_GRAPH = """
""" """
#
# Regions
#
class RegionTable(BaseTable):
pk = ToggleColumn()
# name = tables.LinkColumn(verbose_name='Name')
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
site_count = tables.Column(verbose_name='Sites')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(
template_code=REGION_ACTIONS,
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = Region
fields = ('pk', 'name', 'site_count', 'slug', 'actions')
# #
# Sites # Sites
# #
@ -84,6 +129,7 @@ class SiteTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name') name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
facility = tables.Column(verbose_name='Facility') facility = tables.Column(verbose_name='Facility')
region = tables.TemplateColumn(template_code=SITE_REGION_LINK, verbose_name='Region')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
asn = tables.Column(verbose_name='ASN') asn = tables.Column(verbose_name='ASN')
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks') rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
@ -94,8 +140,10 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Site model = Site
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', fields = (
'vlan_count', 'circuit_count') 'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count',
)
# #

View File

@ -17,6 +17,7 @@ class SiteTest(APITestCase):
'id', 'id',
'name', 'name',
'slug', 'slug',
'region',
'tenant', 'tenant',
'facility', 'facility',
'asn', 'asn',

View File

@ -8,6 +8,12 @@ from . import views
urlpatterns = [ urlpatterns = [
# Regions
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
# Sites # Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'), url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),

View File

@ -26,7 +26,7 @@ from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
RackReservation, RackRole, Site, RackReservation, RackRole, Region, Site,
) )
@ -129,12 +129,37 @@ class ComponentDeleteView(ObjectDeleteView):
return obj.device.get_absolute_url() return obj.device.get_absolute_url()
#
# Regions
#
class RegionListView(ObjectListView):
queryset = Region.objects.annotate(site_count=Count('sites'))
table = tables.RegionTable
template_name = 'dcim/region_list.html'
class RegionEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_region'
model = Region
form_class = forms.RegionForm
def get_return_url(self, obj):
return reverse('dcim:region_list')
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region'
cls = Region
default_return_url = 'dcim:region_list'
# #
# Sites # Sites
# #
class SiteListView(ObjectListView): class SiteListView(ObjectListView):
queryset = Site.objects.select_related('tenant') queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter filter = filters.SiteFilter
filter_form = forms.SiteFilterForm filter_form = forms.SiteFilterForm
table = tables.SiteTable table = tables.SiteTable
@ -143,7 +168,7 @@ class SiteListView(ObjectListView):
def site(request, slug): def site(request, slug):
site = get_object_or_404(Site, slug=slug) site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
stats = { stats = {
'rack_count': Rack.objects.filter(site=site).count(), 'rack_count': Rack.objects.filter(site=site).count(),
'device_count': Device.objects.filter(rack__site=site).count(), 'device_count': Device.objects.filter(rack__site=site).count(),
@ -263,7 +288,7 @@ class RackListView(ObjectListView):
def rack(request, pk): def rack(request, pk):
rack = get_object_or_404(Rack, pk=pk) rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\ nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
.select_related('device_type__manufacturer') .select_related('device_type__manufacturer')
@ -638,7 +663,9 @@ class DeviceListView(ObjectListView):
def device(request, pk): def device(request, pk):
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(Device.objects.select_related(
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
), pk=pk)
console_ports = natsorted( console_ports = natsorted(
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
) )

View File

@ -393,7 +393,9 @@ class PrefixListView(ObjectListView):
def prefix(request, pk): def prefix(request, pk):
prefix = get_object_or_404(Prefix.objects.select_related('site', 'vlan', 'role'), pk=pk) prefix = get_object_or_404(Prefix.objects.select_related(
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
), pk=pk)
try: try:
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix)) aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
@ -731,7 +733,7 @@ class VLANListView(ObjectListView):
def vlan(request, pk): def vlan(request, pk):
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk) vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixBriefTable(list(prefixes)) prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',) prefix_table.exclude = ('vlan',)

View File

@ -104,6 +104,7 @@ INSTALLED_APPS = (
'django.contrib.humanize', 'django.contrib.humanize',
'debug_toolbar', 'debug_toolbar',
'django_tables2', 'django_tables2',
'mptt',
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'rest_framework_swagger',
'circuits', 'circuits',

View File

@ -37,6 +37,11 @@
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li> <li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li>
{% if perms.dcim.add_region %}
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li> <li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
{% if perms.tenancy.add_tenant %} {% if perms.tenancy.add_tenant %}
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li> <li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>

View File

@ -66,6 +66,10 @@
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if circuit.tenant %} {% if circuit.tenant %}
{% if circuit.tenant.group %}
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a> <a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>

View File

@ -27,6 +27,10 @@
<tr> <tr>
<td>Site</td> <td>Site</td>
<td> <td>
{% if termination.site.region %}
<a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a> <a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a>
</td> </td>
</tr> </tr>
@ -34,7 +38,8 @@
<td>Termination</td> <td>Termination</td>
<td> <td>
{% if termination.interface %} {% if termination.interface %}
<span><a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a> {{ termination.interface }}</span> <a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a>
<i class="fa fa-angle-right"></i> {{ termination.interface }}
{% else %} {% else %}
<span class="text-muted">Not defined</span> <span class="text-muted">Not defined</span>
{% endif %} {% endif %}

View File

@ -14,19 +14,13 @@
<strong>Device</strong> <strong>Device</strong>
</div> </div>
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr>
<td>Tenant</td>
<td>
{% if device.tenant %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Site</td> <td>Site</td>
<td> <td>
{% if device.site.region %}
<a href="{{ device.site.region.get_absolute_url }}">{{ device.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a> <a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a>
</td> </td>
</tr> </tr>
@ -34,7 +28,11 @@
<td>Rack</td> <td>Rack</td>
<td> <td>
{% if device.rack %} {% if device.rack %}
<span><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}</span> {% if device.rack.group %}
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}
@ -57,6 +55,20 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<td>Tenant</td>
<td>
{% if device.tenant %}
{% if device.tenant.group %}
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Device Type</td> <td>Device Type</td>
<td> <td>
@ -393,7 +405,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if interfaces or device.device_type.is_network_device %} {% if interfaces or device.device_type.is_network_device %}
{% if perms.dcim.delete_interface %} {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" /> <input type="hidden" name="device" value="{{ device.pk }}" />

View File

@ -64,6 +64,10 @@
<tr> <tr>
<td>Site</td> <td>Site</td>
<td> <td>
{% if rack.site.region %}
<a href="{{ rack.site.region.get_absolute_url }}">{{ rack.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a> <a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a>
</td> </td>
</tr> </tr>
@ -91,6 +95,10 @@
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if rack.tenant %} {% if rack.tenant %}
{% if rack.tenant.group %}
<a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a> <a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>

View File

@ -0,0 +1,21 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Regions{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_region %}
<a href="{% url 'dcim:region_add' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Add a region
</a>
{% endif %}
</div>
<h1>{{ block.title }}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -9,7 +9,12 @@
<div class="row"> <div class="row">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li> {% if site.region %}
{% for region in site.region.get_ancestors %}
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
{% endfor %}
<li><a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
{% endif %}
<li>{{ site }}</li> <li>{{ site }}</li>
</ol> </ol>
</div> </div>
@ -55,10 +60,28 @@
<strong>Site</strong> <strong>Site</strong>
</div> </div>
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr>
<td>Region</td>
<td>
{% if site.region %}
{% for region in site.region.get_ancestors %}
<a href="{{ region.get_absolute_url }}">{{ region }}</a>
<i class="fa fa-angle-right"></i>
{% endfor %}
<a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr> <tr>
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if site.tenant %} {% if site.tenant %}
{% if site.tenant.group %}
<a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a> <a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -85,6 +108,13 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Contact Info</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr> <tr>
<td>Physical Address</td> <td>Physical Address</td>
<td> <td>

View File

@ -7,6 +7,7 @@
<div class="panel-body"> <div class="panel-body">
{% render_field form.name %} {% render_field form.name %}
{% render_field form.slug %} {% render_field form.slug %}
{% render_field form.region %}
{% render_field form.tenant %} {% render_field form.tenant %}
{% render_field form.facility %} {% render_field form.facility %}
{% render_field form.asn %} {% render_field form.asn %}

View File

@ -38,6 +38,11 @@
<td>URL-friendly name</td> <td>URL-friendly name</td>
<td>ash4-south</td> <td>ash4-south</td>
</tr> </tr>
<tr>
<td>Region</td>
<td>Name of region (optional)</td>
<td>North America</td>
</tr>
<tr> <tr>
<td>Tenant</td> <td>Tenant</td>
<td>Name of tenant (optional)</td> <td>Name of tenant (optional)</td>
@ -71,7 +76,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <h4>Example</h4>
<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre> <pre>ASH-4 South,ash4-south,North America,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -30,8 +30,16 @@
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if prefix.tenant %} {% if prefix.tenant %}
{% if prefix.tenant.group %}
<a href="{{ prefix.tenant.group.get_absolute_url }}">{{ prefix.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a> <a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
{% elif prefix.vrf.tenant %} {% elif prefix.vrf.tenant %}
{% if prefix.vrf.tenant.group %}
<a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a> <a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
<label class="label label-info">Inherited</label> <label class="label label-info">Inherited</label>
{% else %} {% else %}
@ -53,6 +61,10 @@
<td>Site</td> <td>Site</td>
<td> <td>
{% if prefix.site %} {% if prefix.site %}
{% if prefix.site.region %}
<a href="{{ prefix.site.region.get_absolute_url }}">{{ prefix.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a> <a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -63,6 +75,10 @@
<td>VLAN</td> <td>VLAN</td>
<td> <td>
{% if prefix.vlan %} {% if prefix.vlan %}
{% if prefix.vlan.group %}
<a href="{{ prefix.vlan.group.get_absolute_url }}">{{ prefix.vlan.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a> <a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -79,7 +95,7 @@
<td>Role</td> <td>Role</td>
<td> <td>
{% if prefix.role %} {% if prefix.role %}
<span>{{ prefix.role }}</span> <a href="{% url 'ipam:prefix_list' %}?role={{ prefix.role.slug }}">{{ prefix.role }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}

View File

@ -57,6 +57,10 @@
<td>Site</td> <td>Site</td>
<td> <td>
{% if vlan.site %} {% if vlan.site %}
{% if vlan.site.region %}
<a href="{{ vlan.site.region.get_absolute_url }}">{{ vlan.site.region }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a> <a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -85,6 +89,10 @@
<td>Tenant</td> <td>Tenant</td>
<td> <td>
{% if vlan.tenant %} {% if vlan.tenant %}
{% if vlan.tenant.group %}
<a href="{{ vlan.tenant.group.get_absolute_url }}">{{ vlan.tenant.group.name }}</a>
<i class="fa fa-angle-right"></i>
{% endif %}
<a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a> <a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
@ -101,7 +109,7 @@
<td>Role</td> <td>Role</td>
<td> <td>
{% if vlan.role %} {% if vlan.role %}
<span>{{ vlan.role }}</span> <a href="{% url 'ipam:vlan_list' %}?role={{ vlan.role.slug }}">{{ vlan.role }}</a>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% endif %} {% endif %}

View File

@ -2,6 +2,8 @@ import csv
import itertools import itertools
import re import re
from mptt.forms import TreeNodeMultipleChoiceField
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
@ -365,7 +367,7 @@ class SlugField(forms.SlugField):
self.widget.attrs['slug-source'] = slug_source self.widget.attrs['slug-source'] = slug_source
class FilterChoiceField(forms.ModelMultipleChoiceField): class FilterChoiceFieldMixin(object):
iterator = forms.models.ModelChoiceIterator iterator = forms.models.ModelChoiceIterator
def __init__(self, null_option=None, *args, **kwargs): def __init__(self, null_option=None, *args, **kwargs):
@ -374,12 +376,13 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
kwargs['required'] = False kwargs['required'] = False
if 'widget' not in kwargs: if 'widget' not in kwargs:
kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6}) kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6})
super(FilterChoiceField, self).__init__(*args, **kwargs) super(FilterChoiceFieldMixin, self).__init__(*args, **kwargs)
def label_from_instance(self, obj): def label_from_instance(self, obj):
label = super(FilterChoiceFieldMixin, self).label_from_instance(obj)
if hasattr(obj, 'filter_count'): if hasattr(obj, 'filter_count'):
return u'{} ({})'.format(obj, obj.filter_count) return u'{} ({})'.format(label, obj.filter_count)
return force_text(obj) return label
def _get_choices(self): def _get_choices(self):
if hasattr(self, '_choices'): if hasattr(self, '_choices'):
@ -391,6 +394,14 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
choices = property(_get_choices, forms.ChoiceField._set_choices) choices = property(_get_choices, forms.ChoiceField._set_choices)
class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField):
pass
class FilterTreeNodeMultipleChoiceField(FilterChoiceFieldMixin, TreeNodeMultipleChoiceField):
pass
class LaxURLField(forms.URLField): class LaxURLField(forms.URLField):
""" """
Custom URLField which allows any valid URL scheme Custom URLField which allows any valid URL scheme

View File

@ -3,6 +3,7 @@ cryptography>=1.4
Django>=1.10 Django>=1.10
django-debug-toolbar>=1.6 django-debug-toolbar>=1.6
django-filter==0.15.3 django-filter==0.15.3
django-mptt==0.8.7
django-rest-swagger==0.3.10 django-rest-swagger==0.3.10
django-tables2>=1.2.5 django-tables2>=1.2.5
djangorestframework>=3.5.0 djangorestframework>=3.5.0