mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
commit
b99704082b
@ -5,7 +5,10 @@ WORKDIR /opt/netbox
|
|||||||
ARG BRANCH=master
|
ARG BRANCH=master
|
||||||
ARG URL=https://github.com/digitalocean/netbox.git
|
ARG URL=https://github.com/digitalocean/netbox.git
|
||||||
RUN git clone --depth 1 $URL -b $BRANCH . && \
|
RUN git clone --depth 1 $URL -b $BRANCH . && \
|
||||||
pip install gunicorn==17.5 && pip install -r requirements.txt
|
apt-get update -qq && apt-get install -y libldap2-dev libsasl2-dev libssl-dev && \
|
||||||
|
pip install gunicorn==17.5 && \
|
||||||
|
pip install django-auth-ldap && \
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
|
ADD docker/docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
ADD netbox/netbox/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
|
ADD netbox/netbox/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py
|
||||||
|
@ -35,6 +35,8 @@ Each export template is associated with a certain type of object. For instance,
|
|||||||
|
|
||||||
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable. Typically, you'll want to iterate through this list using a for loop.
|
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable. Typically, you'll want to iterate through this list using a for loop.
|
||||||
|
|
||||||
|
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||||
|
|
||||||
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
@ -5,6 +5,8 @@ from django.db.models import Q
|
|||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||||
|
|
||||||
from .models import Provider, Circuit, CircuitType
|
from .models import Provider, Circuit, CircuitType
|
||||||
|
|
||||||
|
|
||||||
@ -64,12 +66,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Circuit type (slug)',
|
label='Circuit type (slug)',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
@ -6,7 +6,8 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
|||||||
from tenancy.forms import bulkedit_tenant_choices
|
from tenancy.forms import bulkedit_tenant_choices
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
|
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
|
||||||
|
SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitType, Provider
|
||||||
@ -57,15 +58,9 @@ 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 = forms.MultipleChoiceField(required=False, choices=provider_site_choices,
|
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -189,32 +184,12 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
|
||||||
def circuit_type_choices():
|
|
||||||
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
|
||||||
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def circuit_provider_choices():
|
|
||||||
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
|
|
||||||
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def circuit_tenant_choices():
|
|
||||||
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
|
|
||||||
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def circuit_site_choices():
|
|
||||||
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
|
|
||||||
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
|
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||||
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
|
to_field_name='slug')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
|
to_field_name='slug')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
||||||
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
|
null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuits')), to_field_name='slug')
|
||||||
|
@ -4,6 +4,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
||||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||||
@ -15,12 +16,12 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
action='search',
|
action='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -75,34 +76,34 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
group_id = NullableModelMultipleChoiceFilter(
|
||||||
name='group',
|
name='group',
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
label='Group (ID)',
|
label='Group (ID)',
|
||||||
)
|
)
|
||||||
group = django_filters.ModelMultipleChoiceFilter(
|
group = NullableModelMultipleChoiceFilter(
|
||||||
name='group',
|
name='group',
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Group',
|
label='Group',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
role_id = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role',
|
||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
role = django_filters.ModelMultipleChoiceFilter(
|
role = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role',
|
||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -177,12 +178,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -210,12 +211,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Device model (slug)',
|
label='Device model (slug)',
|
||||||
)
|
)
|
||||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
platform_id = NullableModelMultipleChoiceFilter(
|
||||||
name='platform',
|
name='platform',
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
label='Platform (ID)',
|
label='Platform (ID)',
|
||||||
)
|
)
|
||||||
platform = django_filters.ModelMultipleChoiceFilter(
|
platform = NullableModelMultipleChoiceFilter(
|
||||||
name='platform',
|
name='platform',
|
||||||
queryset=Platform.objects.all(),
|
queryset=Platform.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
@ -9,7 +9,7 @@ from tenancy.forms import bulkedit_tenant_choices
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
|
APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
|
||||||
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -117,15 +117,10 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
|
|
||||||
|
|
||||||
def site_tenant_choices():
|
|
||||||
tenant_choices = Tenant.objects.annotate(site_count=Count('sites'))
|
|
||||||
return [(t.slug, u'{} ({})'.format(t.name, t.site_count)) for t in tenant_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Site
|
model = Site
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=site_tenant_choices,
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
null_option=(0, 'None'))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -140,14 +135,8 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['site', 'name', 'slug']
|
fields = ['site', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
def rackgroup_site_choices():
|
|
||||||
site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
|
|
||||||
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
||||||
site = forms.MultipleChoiceField(required=False, choices=rackgroup_site_choices,
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -254,36 +243,15 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
|
||||||
def rack_site_choices():
|
|
||||||
site_choices = Site.objects.annotate(rack_count=Count('racks'))
|
|
||||||
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def rack_group_choices():
|
|
||||||
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
|
||||||
return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def rack_tenant_choices():
|
|
||||||
tenant_choices = Tenant.objects.annotate(rack_count=Count('racks'))
|
|
||||||
return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def rack_role_choices():
|
|
||||||
role_choices = RackRole.objects.annotate(rack_count=Count('racks'))
|
|
||||||
return [(r.slug, u'{} ({})'.format(r.name, r.rack_count)) for r in role_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Rack
|
model = Rack
|
||||||
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site')
|
||||||
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
|
.annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
|
null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
role = FilterChoiceField(queryset=RackRole.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
|
||||||
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
|
null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -317,14 +285,9 @@ class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
u_height = forms.IntegerField(min_value=1, required=False)
|
u_height = forms.IntegerField(min_value=1, required=False)
|
||||||
|
|
||||||
|
|
||||||
def devicetype_manufacturer_choices():
|
|
||||||
manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
|
||||||
return [(m.slug, u'{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
||||||
manufacturer = forms.MultipleChoiceField(required=False, choices=devicetype_manufacturer_choices,
|
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
to_field_name='slug')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -627,49 +590,18 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||||
|
|
||||||
|
|
||||||
def device_site_choices():
|
|
||||||
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
|
|
||||||
return [(s.slug, u'{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def device_rack_group_choices():
|
|
||||||
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
|
|
||||||
return [(g.pk, u'{} ({})'.format(g, g.device_count)) for g in group_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def device_role_choices():
|
|
||||||
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
|
|
||||||
return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def device_tenant_choices():
|
|
||||||
tenant_choices = Tenant.objects.annotate(device_count=Count('devices'))
|
|
||||||
return [(t.slug, u'{} ({})'.format(t.name, t.device_count)) for t in tenant_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def device_type_choices():
|
|
||||||
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
|
|
||||||
return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def device_platform_choices():
|
|
||||||
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
|
|
||||||
return [(p.slug, u'{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Device
|
model = Device
|
||||||
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
rack_group_id = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
|
||||||
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group',
|
label='Rack Group')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
role = FilterChoiceField(queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), to_field_name='slug')
|
||||||
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
null_option=(0, 'None'))
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices,
|
device_type_id = FilterChoiceField(queryset=DeviceType.objects.select_related('manufacturer')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
.annotate(filter_count=Count('instances')), label='Type')
|
||||||
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
|
platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')),
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
to_field_name='slug', null_option=(0, 'None'))
|
||||||
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
|
|
||||||
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
|
status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1399,7 +1399,7 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
|||||||
template_name = 'dcim/interface_add_multi.html'
|
template_name = 'dcim/interface_add_multi.html'
|
||||||
default_redirect_url = 'dcim:device_list'
|
default_redirect_url = 'dcim:device_list'
|
||||||
|
|
||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form, fields):
|
||||||
|
|
||||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||||
interfaces = []
|
interfaces = []
|
||||||
|
@ -2,7 +2,7 @@ import django_filters
|
|||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from .models import CustomField
|
from .models import CF_TYPE_SELECT, CustomField
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilter(django_filters.Filter):
|
class CustomFieldFilter(django_filters.Filter):
|
||||||
@ -10,9 +10,22 @@ class CustomFieldFilter(django_filters.Filter):
|
|||||||
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cf_type, *args, **kwargs):
|
||||||
|
self.cf_type = cf_type
|
||||||
|
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def filter(self, queryset, value):
|
def filter(self, queryset, value):
|
||||||
|
# Skip filter on empty value
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
|
# Treat 0 as None for Select fields
|
||||||
|
try:
|
||||||
|
if self.cf_type == CF_TYPE_SELECT and int(value) == 0:
|
||||||
|
return queryset.exclude(
|
||||||
|
custom_field_values__field__name=self.name,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
custom_field_values__field__name=self.name,
|
custom_field_values__field__name=self.name,
|
||||||
custom_field_values__serialized_value=value,
|
custom_field_values__serialized_value=value,
|
||||||
@ -30,4 +43,4 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
|||||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||||
custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
|
custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
|
||||||
for cf in custom_fields:
|
for cf in custom_fields:
|
||||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name)
|
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, cf_type=cf.type)
|
||||||
|
@ -47,14 +47,12 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
|
|
||||||
# Select
|
# Select
|
||||||
elif cf.type == CF_TYPE_SELECT:
|
elif cf.type == CF_TYPE_SELECT:
|
||||||
if bulk_edit:
|
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
if not cf.required:
|
||||||
if not cf.required:
|
choices = [(0, 'None')] + choices
|
||||||
choices = [(0, 'None')] + choices
|
if bulk_edit or filterable_only:
|
||||||
choices = [(None, '---------')] + choices
|
choices = [(None, '---------')] + choices
|
||||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
||||||
else:
|
|
||||||
field = forms.ModelChoiceField(queryset=cf.choices.all(), required=cf.required)
|
|
||||||
|
|
||||||
# URL
|
# URL
|
||||||
elif cf.type == CF_TYPE_URL:
|
elif cf.type == CF_TYPE_URL:
|
||||||
|
@ -67,7 +67,18 @@ ACTION_CHOICES = (
|
|||||||
|
|
||||||
class CustomFieldModel(object):
|
class CustomFieldModel(object):
|
||||||
|
|
||||||
def custom_fields(self):
|
def cf(self):
|
||||||
|
"""
|
||||||
|
Name-based CustomFieldValue accessor for use in templates
|
||||||
|
"""
|
||||||
|
if not hasattr(self, 'custom_fields'):
|
||||||
|
return dict()
|
||||||
|
return {field.name: value for field, value in self.custom_fields.items()}
|
||||||
|
|
||||||
|
def get_custom_fields(self):
|
||||||
|
"""
|
||||||
|
Return a dictionary of custom fields for a single object in the form {<field>: value}.
|
||||||
|
"""
|
||||||
|
|
||||||
# Find all custom fields applicable to this type of object
|
# Find all custom fields applicable to this type of object
|
||||||
content_type = ContentType.objects.get_for_model(self)
|
content_type = ContentType.objects.get_for_model(self)
|
||||||
|
@ -7,6 +7,7 @@ from django.db.models import Q
|
|||||||
from dcim.models import Site, Device, Interface
|
from dcim.models import Site, Device, Interface
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||||
|
|
||||||
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
|
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
|
||||||
|
|
||||||
@ -21,12 +22,12 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
lookup_type='icontains',
|
lookup_type='icontains',
|
||||||
label='Name',
|
label='Name',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -85,29 +86,34 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
action='search_by_parent',
|
action='search_by_parent',
|
||||||
label='Parent prefix',
|
label='Parent prefix',
|
||||||
)
|
)
|
||||||
vrf = django_filters.MethodFilter(
|
vrf_id = NullableModelMultipleChoiceFilter(
|
||||||
action='_vrf',
|
name='vrf_id',
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
label='VRF',
|
label='VRF',
|
||||||
)
|
)
|
||||||
# Duplicate of `vrf` for backward-compatibility
|
vrf = NullableModelMultipleChoiceFilter(
|
||||||
vrf_id = django_filters.MethodFilter(
|
name='vrf',
|
||||||
action='_vrf',
|
queryset=VRF.objects.all(),
|
||||||
label='VRF',
|
to_field_name='rd',
|
||||||
|
label='VRF (RD)',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.MethodFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
action='_tenant_id',
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = django_filters.MethodFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
action='_tenant',
|
name='tenant',
|
||||||
label='Tenant',
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -122,12 +128,12 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
name='vlan__vid',
|
name='vlan__vid',
|
||||||
label='VLAN number (1-4095)',
|
label='VLAN number (1-4095)',
|
||||||
)
|
)
|
||||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
role_id = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role',
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
role = django_filters.ModelMultipleChoiceFilter(
|
role = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role',
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -136,7 +142,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
|
fields = ['family', 'site_id', 'site', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
@ -157,17 +163,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
def _vrf(self, queryset, value):
|
|
||||||
if str(value) == '':
|
|
||||||
return queryset
|
|
||||||
try:
|
|
||||||
vrf_id = int(value)
|
|
||||||
except ValueError:
|
|
||||||
return queryset.none()
|
|
||||||
if vrf_id == 0:
|
|
||||||
return queryset.filter(vrf__isnull=True)
|
|
||||||
return queryset.filter(vrf__pk=value)
|
|
||||||
|
|
||||||
def _tenant(self, queryset, value):
|
def _tenant(self, queryset, value):
|
||||||
if str(value) == '':
|
if str(value) == '':
|
||||||
return queryset
|
return queryset
|
||||||
@ -196,22 +191,27 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
action='search_by_parent',
|
action='search_by_parent',
|
||||||
label='Parent prefix',
|
label='Parent prefix',
|
||||||
)
|
)
|
||||||
vrf = django_filters.MethodFilter(
|
vrf_id = NullableModelMultipleChoiceFilter(
|
||||||
action='_vrf',
|
name='vrf_id',
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
label='VRF',
|
label='VRF',
|
||||||
)
|
)
|
||||||
# Duplicate of `vrf` for backward-compatibility
|
vrf = NullableModelMultipleChoiceFilter(
|
||||||
vrf_id = django_filters.MethodFilter(
|
name='vrf',
|
||||||
action='_vrf',
|
queryset=VRF.objects.all(),
|
||||||
label='VRF',
|
to_field_name='rd',
|
||||||
|
label='VRF (RD)',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.MethodFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
action='_tenant_id',
|
name='tenant',
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = django_filters.MethodFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
action='_tenant',
|
name='tenant',
|
||||||
label='Tenant',
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='interface__device',
|
name='interface__device',
|
||||||
@ -232,7 +232,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
|
fields = ['q', 'family', 'device_id', 'device', 'interface_id']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
@ -253,35 +253,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
except AddrFormatError:
|
except AddrFormatError:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
def _vrf(self, queryset, value):
|
|
||||||
if str(value) == '':
|
|
||||||
return queryset
|
|
||||||
try:
|
|
||||||
vrf_id = int(value)
|
|
||||||
except ValueError:
|
|
||||||
return queryset.none()
|
|
||||||
if vrf_id == 0:
|
|
||||||
return queryset.filter(vrf__isnull=True)
|
|
||||||
return queryset.filter(vrf__pk=value)
|
|
||||||
|
|
||||||
def _tenant(self, queryset, value):
|
|
||||||
if str(value) == '':
|
|
||||||
return queryset
|
|
||||||
return queryset.filter(
|
|
||||||
Q(tenant__slug=value) |
|
|
||||||
Q(tenant__isnull=True, vrf__tenant__slug=value)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _tenant_id(self, queryset, value):
|
|
||||||
try:
|
|
||||||
value = int(value)
|
|
||||||
except ValueError:
|
|
||||||
return queryset.none()
|
|
||||||
return queryset.filter(
|
|
||||||
Q(tenant__pk=value) |
|
|
||||||
Q(tenant__isnull=True, vrf__tenant__pk=value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupFilter(django_filters.FilterSet):
|
class VLANGroupFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
@ -317,12 +288,12 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
group_id = NullableModelMultipleChoiceFilter(
|
||||||
name='group',
|
name='group',
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
label='Group (ID)',
|
label='Group (ID)',
|
||||||
)
|
)
|
||||||
group = django_filters.ModelMultipleChoiceFilter(
|
group = NullableModelMultipleChoiceFilter(
|
||||||
name='group',
|
name='group',
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -337,23 +308,23 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
name='vid',
|
name='vid',
|
||||||
label='VLAN number (1-4095)',
|
label='VLAN number (1-4095)',
|
||||||
)
|
)
|
||||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
name='tenant',
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
role_id = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role',
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
role = django_filters.ModelMultipleChoiceFilter(
|
role = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role',
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
@ -5,7 +5,9 @@ from dcim.models import Site, Device, Interface
|
|||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from tenancy.forms import bulkedit_tenant_choices
|
from tenancy.forms import bulkedit_tenant_choices
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
|
from utilities.forms import (
|
||||||
|
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||||
@ -69,15 +71,10 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
def vrf_tenant_choices():
|
|
||||||
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs'))
|
|
||||||
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = VRF
|
model = VRF
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices,
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
null_option=(0, None))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -128,16 +125,11 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
def aggregate_rir_choices():
|
|
||||||
rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
|
||||||
return [(r.slug, u'{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||||
rir = forms.MultipleChoiceField(required=False, choices=aggregate_rir_choices, label='RIR',
|
rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
label='RIR')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -268,21 +260,6 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
def prefix_vrf_choices():
|
|
||||||
vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
|
|
||||||
return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def tenant_choices():
|
|
||||||
tenant_choices = Tenant.objects.all()
|
|
||||||
return [(t.slug, t.name) for t in tenant_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def prefix_site_choices():
|
|
||||||
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
|
|
||||||
return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def prefix_status_choices():
|
def prefix_status_choices():
|
||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
@ -290,27 +267,21 @@ def prefix_status_choices():
|
|||||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
def prefix_role_choices():
|
|
||||||
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
|
|
||||||
return [(r.slug, u'{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||||
'placeholder': 'Network',
|
'placeholder': 'Network',
|
||||||
}))
|
}))
|
||||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||||
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
label='VRF', null_option=(0, 'Global'))
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
null_option=(0, 'None'))
|
||||||
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
|
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||||
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
|
null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||||
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
|
null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
|
||||||
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
||||||
|
|
||||||
|
|
||||||
@ -441,21 +412,16 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
def ipaddress_vrf_choices():
|
|
||||||
vrf_choices = VRF.objects.annotate(ipaddress_count=Count('ip_addresses'))
|
|
||||||
return [(v.pk, u'{} ({})'.format(v.name, v.ipaddress_count)) for v in vrf_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||||
'placeholder': 'Prefix',
|
'placeholder': 'Prefix',
|
||||||
}))
|
}))
|
||||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||||
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
|
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='rd',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
label='VRF', null_option=(0, 'Global'))
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
to_field_name='slug', null_option=(0, 'None'))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -470,14 +436,8 @@ class VLANGroupForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['site', 'name', 'slug']
|
fields = ['site', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
def vlangroup_site_choices():
|
|
||||||
site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
|
|
||||||
return [(s.slug, u'{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
|
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
|
||||||
site = forms.MultipleChoiceField(required=False, choices=vlangroup_site_choices,
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -555,21 +515,6 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
def vlan_site_choices():
|
|
||||||
site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
|
|
||||||
return [(s.slug, u'{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def vlan_group_choices():
|
|
||||||
group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
|
|
||||||
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def vlan_tenant_choices():
|
|
||||||
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans'))
|
|
||||||
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
|
|
||||||
|
|
||||||
|
|
||||||
def vlan_status_choices():
|
def vlan_status_choices():
|
||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
@ -577,19 +522,13 @@ def vlan_status_choices():
|
|||||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
def vlan_role_choices():
|
|
||||||
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
|
|
||||||
return [(r.slug, u'{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
|
||||||
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
|
null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices,
|
null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
||||||
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
|
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||||
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
|
null_option=(0, 'None'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
19
netbox/ipam/migrations/0008_prefix_change_order.py
Normal file
19
netbox/ipam/migrations/0008_prefix_change_order.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10 on 2016-09-15 16:08
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0007_prefix_ipaddress_add_tenant'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='prefix',
|
||||||
|
options={'ordering': ['vrf', 'family', 'prefix'], 'verbose_name_plural': 'prefixes'},
|
||||||
|
),
|
||||||
|
]
|
@ -12,6 +12,7 @@ from dcim.models import Interface
|
|||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
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
|
||||||
|
from utilities.sql import NullsFirstQuerySet
|
||||||
|
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
|
|
||||||
@ -192,7 +193,7 @@ class Role(models.Model):
|
|||||||
return self.vlans.count()
|
return self.vlans.count()
|
||||||
|
|
||||||
|
|
||||||
class PrefixQuerySet(models.QuerySet):
|
class PrefixQuerySet(NullsFirstQuerySet):
|
||||||
|
|
||||||
def annotate_depth(self, limit=None):
|
def annotate_depth(self, limit=None):
|
||||||
"""
|
"""
|
||||||
@ -249,7 +250,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
objects = PrefixQuerySet.as_manager()
|
objects = PrefixQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['family', 'prefix']
|
ordering = ['vrf', 'family', 'prefix']
|
||||||
verbose_name_plural = 'prefixes'
|
verbose_name_plural = 'prefixes'
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
|
@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.6.0'
|
VERSION = '1.6.1'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
@ -71,7 +71,7 @@ if LDAP_CONFIGURED:
|
|||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImproperlyConfigured("LDAP authentication has been configured, but django-auth-ldap is not installed. "
|
raise ImproperlyConfigured("LDAP authentication has been configured, but django-auth-ldap is not installed. "
|
||||||
"You can remove netbox/ldap.py to disable LDAP.")
|
"You can remove netbox/ldap_config.py to disable LDAP.")
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
if (slug_field) {
|
if (slug_field) {
|
||||||
var slug_source = $('#id_' + slug_field.attr('slug-source'));
|
var slug_source = $('#id_' + slug_field.attr('slug-source'));
|
||||||
slug_source.keyup(function() {
|
slug_source.on('keyup change', function() {
|
||||||
if (slug_field && !slug_field.attr('_changed')) {
|
if (slug_field && !slug_field.attr('_changed')) {
|
||||||
slug_field.val(slugify($(this).val(), 50));
|
slug_field.val(slugify($(this).val(), 50));
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ from django import forms
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, SlugField
|
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
||||||
|
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
@ -95,13 +95,8 @@ class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
|||||||
name = forms.CharField(max_length=100, required=False)
|
name = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
def secret_role_choices():
|
|
||||||
role_choices = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
|
||||||
return [(r.slug, u'{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class SecretFilterForm(forms.Form, BootstrapMixin):
|
class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||||
role = forms.MultipleChoiceField(required=False, choices=secret_role_choices)
|
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -104,6 +104,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% with circuit.get_custom_fields as custom_fields %}
|
||||||
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=circuit %}
|
{% include 'inc/created_updated.html' with obj=circuit %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with provider.custom_fields as custom_fields %}
|
{% with provider.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -144,7 +144,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with device.custom_fields as custom_fields %}
|
{% with device.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
|
@ -132,7 +132,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with rack.custom_fields as custom_fields %}
|
{% with rack.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with site.custom_fields as custom_fields %}
|
{% with site.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
{% include 'inc/created_updated.html' with obj=aggregate %}
|
{% include 'inc/created_updated.html' with obj=aggregate %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% with aggregate.custom_fields as custom_fields %}
|
{% with aggregate.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -121,6 +121,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% with ipaddress.get_custom_fields as custom_fields %}
|
||||||
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -101,6 +101,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% with prefix.get_custom_fields as custom_fields %}
|
||||||
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=prefix %}
|
{% include 'inc/created_updated.html' with obj=prefix %}
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,15 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
<a href="{% url 'ipam:prefix_list' %}{% querystring_toggle request expand='on' %}" class="btn btn-default">
|
||||||
|
{% if 'expand' in request.GET %}
|
||||||
|
<span class="fa fa-chevron-right" aria-hidden="true"></span>
|
||||||
|
Collapse all
|
||||||
|
{% else %}
|
||||||
|
<span class="fa fa-chevron-down" aria-hidden="true"></span>
|
||||||
|
Expand all
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
{% if perms.ipam.add_prefix %}
|
{% if perms.ipam.add_prefix %}
|
||||||
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
|
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
|
||||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
|
@ -110,6 +110,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% with vlan.get_custom_fields as custom_fields %}
|
||||||
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=vlan %}
|
{% include 'inc/created_updated.html' with obj=vlan %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -82,6 +82,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% with vrf.get_custom_fields as custom_fields %}
|
||||||
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with tenant.custom_fields as custom_fields %}
|
{% with tenant.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -3,6 +3,7 @@ import django_filters
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
|
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
@ -11,12 +12,12 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
action='search',
|
action='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
group_id = NullableModelMultipleChoiceFilter(
|
||||||
name='group',
|
name='group',
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
label='Group (ID)',
|
label='Group (ID)',
|
||||||
)
|
)
|
||||||
group = django_filters.ModelMultipleChoiceFilter(
|
group = NullableModelMultipleChoiceFilter(
|
||||||
name='group',
|
name='group',
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
@ -2,7 +2,7 @@ from django import forms
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDataField, SlugField
|
from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, SlugField
|
||||||
|
|
||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
@ -74,12 +74,7 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group')
|
group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group')
|
||||||
|
|
||||||
|
|
||||||
def tenant_group_choices():
|
|
||||||
group_choices = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
|
|
||||||
return [(g.slug, u'{} ({})'.format(g.name, g.tenant_count)) for g in group_choices]
|
|
||||||
|
|
||||||
|
|
||||||
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Tenant
|
model = Tenant
|
||||||
group = forms.MultipleChoiceField(required=False, choices=tenant_group_choices,
|
group = FilterChoiceField(queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
to_field_name='slug', null_option=(0, 'None'))
|
||||||
|
93
netbox/utilities/filters.py
Normal file
93
netbox/utilities/filters.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import django_filters
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
|
|
||||||
|
class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
|
"""
|
||||||
|
This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is
|
||||||
|
used to represent a value of Null. This is accomplished by creating a new iterator which first yields the null
|
||||||
|
choice before entering the queryset iterator, and by ignoring the null choice during cleaning. The effect is similar
|
||||||
|
to defining a MultipleChoiceField with:
|
||||||
|
|
||||||
|
choices = [(0, 'None')] + [(x.id, x) for x in Foo.objects.all()]
|
||||||
|
|
||||||
|
However, the above approach forces immediate evaluation of the queryset, which can cause issues when calculating
|
||||||
|
database migrations.
|
||||||
|
"""
|
||||||
|
iterator = forms.models.ModelChoiceIterator
|
||||||
|
|
||||||
|
def __init__(self, null_value=0, null_label='None', *args, **kwargs):
|
||||||
|
self.null_value = null_value
|
||||||
|
self.null_label = null_label
|
||||||
|
super(NullableModelMultipleChoiceField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _get_choices(self):
|
||||||
|
if hasattr(self, '_choices'):
|
||||||
|
return self._choices
|
||||||
|
# Prepend the null choice to the queryset iterator
|
||||||
|
return itertools.chain(
|
||||||
|
[(self.null_value, self.null_label)],
|
||||||
|
self.iterator(self),
|
||||||
|
)
|
||||||
|
choices = property(_get_choices, forms.ChoiceField._set_choices)
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
# Strip all instances of the null value before cleaning
|
||||||
|
if value is not None:
|
||||||
|
stripped_value = [x for x in value if x != force_text(self.null_value)]
|
||||||
|
else:
|
||||||
|
stripped_value = value
|
||||||
|
super(NullableModelMultipleChoiceField, self).clean(stripped_value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class NullableModelMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
|
||||||
|
"""
|
||||||
|
This class extends ModelMultipleChoiceFilter to accept an additional value which implies "is null". The default
|
||||||
|
queryset filter argument is:
|
||||||
|
|
||||||
|
.filter(fieldname=value)
|
||||||
|
|
||||||
|
When filtering by the value representing "is null" ('0' by default) the argument is modified to:
|
||||||
|
|
||||||
|
.filter(fieldname__isnull=True)
|
||||||
|
"""
|
||||||
|
field_class = NullableModelMultipleChoiceField
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.null_value = kwargs.get('null_value', 0)
|
||||||
|
super(NullableModelMultipleChoiceFilter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def filter(self, qs, value):
|
||||||
|
value = value or () # Make sure we have an iterable
|
||||||
|
|
||||||
|
if self.is_noop(qs, value):
|
||||||
|
return qs
|
||||||
|
|
||||||
|
# Even though not a noop, no point filtering if empty
|
||||||
|
if not value:
|
||||||
|
return qs
|
||||||
|
|
||||||
|
q = Q()
|
||||||
|
for v in set(value):
|
||||||
|
# Filtering by "is null"
|
||||||
|
if v == force_text(self.null_value):
|
||||||
|
arg = {'{}__isnull'.format(self.name): True}
|
||||||
|
# Filtering by a related field (e.g. slug)
|
||||||
|
elif self.field.to_field_name is not None:
|
||||||
|
arg = {'{}__{}'.format(self.name, self.field.to_field_name): v}
|
||||||
|
# Filtering by primary key (default)
|
||||||
|
else:
|
||||||
|
arg = {self.name: v}
|
||||||
|
if self.conjoined:
|
||||||
|
qs = self.get_method(qs)(**arg)
|
||||||
|
else:
|
||||||
|
q |= Q(**arg)
|
||||||
|
if self.distinct:
|
||||||
|
return self.get_method(qs)(q).distinct()
|
||||||
|
|
||||||
|
return self.get_method(qs)(q)
|
@ -1,4 +1,5 @@
|
|||||||
import csv
|
import csv
|
||||||
|
import itertools
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -142,10 +143,14 @@ class CSVDataField(forms.CharField):
|
|||||||
if not self.help_text:
|
if not self.help_text:
|
||||||
self.help_text = 'Enter one line per record in CSV format.'
|
self.help_text = 'Enter one line per record in CSV format.'
|
||||||
|
|
||||||
|
def utf_8_encoder(self, unicode_csv_data):
|
||||||
|
for line in unicode_csv_data:
|
||||||
|
yield line.encode('utf-8')
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
# Return a list of dictionaries, each representing an individual record
|
# Return a list of dictionaries, each representing an individual record
|
||||||
records = []
|
records = []
|
||||||
reader = csv.reader(value.splitlines())
|
reader = csv.reader(self.utf_8_encoder(value.splitlines()))
|
||||||
for i, row in enumerate(reader, start=1):
|
for i, row in enumerate(reader, start=1):
|
||||||
if row:
|
if row:
|
||||||
if len(row) < len(self.columns):
|
if len(row) < len(self.columns):
|
||||||
@ -222,6 +227,32 @@ class SlugField(forms.SlugField):
|
|||||||
self.widget.attrs['slug-source'] = slug_source
|
self.widget.attrs['slug-source'] = slug_source
|
||||||
|
|
||||||
|
|
||||||
|
class FilterChoiceField(forms.ModelMultipleChoiceField):
|
||||||
|
iterator = forms.models.ModelChoiceIterator
|
||||||
|
|
||||||
|
def __init__(self, null_option=None, *args, **kwargs):
|
||||||
|
self.null_option = null_option
|
||||||
|
if 'required' not in kwargs:
|
||||||
|
kwargs['required'] = False
|
||||||
|
if 'widget' not in kwargs:
|
||||||
|
kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6})
|
||||||
|
super(FilterChoiceField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def label_from_instance(self, obj):
|
||||||
|
if hasattr(obj, 'filter_count'):
|
||||||
|
return u'{} ({})'.format(obj, obj.filter_count)
|
||||||
|
return force_text(obj)
|
||||||
|
|
||||||
|
def _get_choices(self):
|
||||||
|
if hasattr(self, '_choices'):
|
||||||
|
return self._choices
|
||||||
|
if self.null_option is not None:
|
||||||
|
return itertools.chain([self.null_option], self.iterator(self))
|
||||||
|
return self.iterator(self)
|
||||||
|
|
||||||
|
choices = property(_get_choices, forms.ChoiceField._set_choices)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Forms
|
# Forms
|
||||||
#
|
#
|
||||||
|
32
netbox/utilities/sql.py
Normal file
32
netbox/utilities/sql.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import connections, models
|
||||||
|
from django.db.models.sql.compiler import SQLCompiler
|
||||||
|
|
||||||
|
|
||||||
|
class NullsFirstSQLCompiler(SQLCompiler):
|
||||||
|
|
||||||
|
def get_order_by(self):
|
||||||
|
result = super(NullsFirstSQLCompiler, self).get_order_by()
|
||||||
|
if result:
|
||||||
|
return [(expr, (sql + ' NULLS FIRST', params, is_ref)) for (expr, (sql, params, is_ref)) in result]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class NullsFirstQuery(models.sql.query.Query):
|
||||||
|
|
||||||
|
def get_compiler(self, using=None, connection=None):
|
||||||
|
if using is None and connection is None:
|
||||||
|
raise ValueError("Need either using or connection")
|
||||||
|
if using:
|
||||||
|
connection = connections[using]
|
||||||
|
return NullsFirstSQLCompiler(self, connection, using)
|
||||||
|
|
||||||
|
|
||||||
|
class NullsFirstQuerySet(models.QuerySet):
|
||||||
|
"""
|
||||||
|
Override PostgreSQL's default behavior of ordering NULLs last. This is needed e.g. to order Prefixes in the global
|
||||||
|
table before those assigned to a VRF.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model=None, query=None, using=None, hints=None):
|
||||||
|
super(NullsFirstQuerySet, self).__init__(model, query, using, hints)
|
||||||
|
self.query = query or NullsFirstQuery(self.model)
|
@ -1,3 +1,4 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -10,18 +11,30 @@ from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, TypedCho
|
|||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template import TemplateSyntaxError
|
from django.template import TemplateSyntaxError
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm
|
from extras.forms import CustomFieldForm
|
||||||
from extras.models import CustomFieldValue, ExportTemplate, UserAction
|
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
|
||||||
|
|
||||||
from .error_handlers import handle_protectederror
|
from .error_handlers import handle_protectederror
|
||||||
from .forms import ConfirmationForm
|
from .forms import ConfirmationForm
|
||||||
from .paginator import EnhancedPaginator
|
from .paginator import EnhancedPaginator
|
||||||
|
|
||||||
|
|
||||||
|
class annotate_custom_fields:
|
||||||
|
|
||||||
|
def __init__(self, queryset, custom_fields):
|
||||||
|
self.queryset = queryset
|
||||||
|
self.custom_fields = custom_fields
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for obj in self.queryset:
|
||||||
|
values_dict = {cfv.field_id: cfv.value for cfv in obj.custom_field_values.all()}
|
||||||
|
obj.custom_fields = OrderedDict([(field, values_dict.get(field.pk)) for field in self.custom_fields])
|
||||||
|
yield obj
|
||||||
|
|
||||||
|
|
||||||
class ObjectListView(View):
|
class ObjectListView(View):
|
||||||
queryset = None
|
queryset = None
|
||||||
filter = None
|
filter = None
|
||||||
@ -39,19 +52,26 @@ class ObjectListView(View):
|
|||||||
if self.filter:
|
if self.filter:
|
||||||
self.queryset = self.filter(request.GET, self.queryset).qs
|
self.queryset = self.filter(request.GET, self.queryset).qs
|
||||||
|
|
||||||
|
# If this type of object has one or more custom fields, prefetch any relevant custom field values
|
||||||
|
custom_fields = CustomField.objects.filter(obj_type=ContentType.objects.get_for_model(model))\
|
||||||
|
.prefetch_related('choices')
|
||||||
|
if custom_fields:
|
||||||
|
self.queryset = self.queryset.prefetch_related('custom_field_values')
|
||||||
|
|
||||||
# Check for export template rendering
|
# Check for export template rendering
|
||||||
if request.GET.get('export'):
|
if request.GET.get('export'):
|
||||||
et = get_object_or_404(ExportTemplate, content_type=object_ct, name=request.GET.get('export'))
|
et = get_object_or_404(ExportTemplate, content_type=object_ct, name=request.GET.get('export'))
|
||||||
|
queryset = annotate_custom_fields(self.queryset, custom_fields) if custom_fields else self.queryset
|
||||||
try:
|
try:
|
||||||
response = et.to_response(context_dict={'queryset': self.queryset.all()},
|
response = et.to_response(context_dict={'queryset': queryset},
|
||||||
filename='netbox_{}'.format(self.queryset.model._meta.verbose_name_plural))
|
filename='netbox_{}'.format(model._meta.verbose_name_plural))
|
||||||
return response
|
return response
|
||||||
except TemplateSyntaxError:
|
except TemplateSyntaxError:
|
||||||
messages.error(request, "There was an error rendering the selected export template ({})."
|
messages.error(request, "There was an error rendering the selected export template ({})."
|
||||||
.format(et.name))
|
.format(et.name))
|
||||||
# Fall back to built-in CSV export
|
# Fall back to built-in CSV export
|
||||||
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
elif 'export' in request.GET and hasattr(model, 'to_csv'):
|
||||||
output = '\n'.join([obj.to_csv() for obj in self.queryset.all()])
|
output = '\n'.join([obj.to_csv() for obj in self.queryset])
|
||||||
response = HttpResponse(
|
response = HttpResponse(
|
||||||
output,
|
output,
|
||||||
content_type='text/csv'
|
content_type='text/csv'
|
||||||
|
0
scripts/docker-build.sh
Normal file → Executable file
0
scripts/docker-build.sh
Normal file → Executable file
Loading…
Reference in New Issue
Block a user