mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
commit
b99704082b
@ -5,7 +5,10 @@ WORKDIR /opt/netbox
|
||||
ARG BRANCH=master
|
||||
ARG URL=https://github.com/digitalocean/netbox.git
|
||||
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 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.
|
||||
|
||||
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`.
|
||||
|
||||
## Example
|
||||
|
@ -5,6 +5,8 @@ from django.db.models import Q
|
||||
from dcim.models import Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
|
||||
from .models import Provider, Circuit, CircuitType
|
||||
|
||||
|
||||
@ -64,12 +66,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Circuit type (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
|
@ -6,7 +6,8 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
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
|
||||
@ -57,15 +58,9 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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):
|
||||
model = Provider
|
||||
site = forms.MultipleChoiceField(required=False, choices=provider_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
#
|
||||
@ -189,32 +184,12 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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):
|
||||
model = Circuit
|
||||
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
|
||||
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
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 tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from .models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||
@ -15,12 +16,12 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
@ -75,34 +76,34 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
role_id = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=RackRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
role = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=RackRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
@ -177,12 +178,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
@ -210,12 +211,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Device model (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
platform_id = NullableModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform (ID)',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
platform = NullableModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
|
@ -9,7 +9,7 @@ from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
|
||||
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
@ -117,15 +117,10 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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):
|
||||
model = Site
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=site_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
|
||||
|
||||
#
|
||||
@ -140,14 +135,8 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
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):
|
||||
site = forms.MultipleChoiceField(required=False, choices=rackgroup_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug')
|
||||
|
||||
|
||||
#
|
||||
@ -254,36 +243,15 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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):
|
||||
model = Rack
|
||||
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug')
|
||||
group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site')
|
||||
.annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
role = FilterChoiceField(queryset=RackRole.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
|
||||
|
||||
#
|
||||
@ -317,14 +285,9 @@ class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
||||
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):
|
||||
manufacturer = forms.MultipleChoiceField(required=False, choices=devicetype_manufacturer_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||
to_field_name='slug')
|
||||
|
||||
|
||||
#
|
||||
@ -627,49 +590,18 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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):
|
||||
model = Device
|
||||
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
|
||||
rack_group_id = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
|
||||
label='Rack Group')
|
||||
role = FilterChoiceField(queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), to_field_name='slug')
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
device_type_id = FilterChoiceField(queryset=DeviceType.objects.select_related('manufacturer')
|
||||
.annotate(filter_count=Count('instances')), label='Type')
|
||||
platform = FilterChoiceField(queryset=Platform.objects.annotate(filter_count=Count('devices')),
|
||||
to_field_name='slug', null_option=(0, 'None'))
|
||||
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'
|
||||
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)
|
||||
interfaces = []
|
||||
|
@ -2,7 +2,7 @@ import django_filters
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from .models import CustomField
|
||||
from .models import CF_TYPE_SELECT, CustomField
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, cf_type, *args, **kwargs):
|
||||
self.cf_type = cf_type
|
||||
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, queryset, value):
|
||||
# Skip filter on empty value
|
||||
if not value.strip():
|
||||
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(
|
||||
custom_field_values__field__name=self.name,
|
||||
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)
|
||||
custom_fields = CustomField.objects.filter(obj_type=obj_type, is_filterable=True)
|
||||
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
|
||||
elif cf.type == CF_TYPE_SELECT:
|
||||
if bulk_edit:
|
||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||
if not cf.required:
|
||||
choices = [(0, 'None')] + choices
|
||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||
if not cf.required:
|
||||
choices = [(0, 'None')] + choices
|
||||
if bulk_edit or filterable_only:
|
||||
choices = [(None, '---------')] + choices
|
||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
||||
else:
|
||||
field = forms.ModelChoiceField(queryset=cf.choices.all(), required=cf.required)
|
||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
||||
|
||||
# URL
|
||||
elif cf.type == CF_TYPE_URL:
|
||||
|
@ -67,7 +67,18 @@ ACTION_CHOICES = (
|
||||
|
||||
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
|
||||
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 extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
|
||||
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
|
||||
|
||||
@ -21,12 +22,12 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
lookup_type='icontains',
|
||||
label='Name',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
@ -85,29 +86,34 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
action='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
vrf = django_filters.MethodFilter(
|
||||
action='_vrf',
|
||||
vrf_id = NullableModelMultipleChoiceFilter(
|
||||
name='vrf_id',
|
||||
queryset=VRF.objects.all(),
|
||||
label='VRF',
|
||||
)
|
||||
# Duplicate of `vrf` for backward-compatibility
|
||||
vrf_id = django_filters.MethodFilter(
|
||||
action='_vrf',
|
||||
label='VRF',
|
||||
vrf = NullableModelMultipleChoiceFilter(
|
||||
name='vrf',
|
||||
queryset=VRF.objects.all(),
|
||||
to_field_name='rd',
|
||||
label='VRF (RD)',
|
||||
)
|
||||
tenant_id = django_filters.MethodFilter(
|
||||
action='_tenant_id',
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.MethodFilter(
|
||||
action='_tenant',
|
||||
label='Tenant',
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
site_id = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
site = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
@ -122,12 +128,12 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
name='vlan__vid',
|
||||
label='VLAN number (1-4095)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
role_id = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
role = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
to_field_name='slug',
|
||||
@ -136,7 +142,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
@ -157,17 +163,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
except AddrFormatError:
|
||||
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
|
||||
@ -196,22 +191,27 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
action='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
vrf = django_filters.MethodFilter(
|
||||
action='_vrf',
|
||||
vrf_id = NullableModelMultipleChoiceFilter(
|
||||
name='vrf_id',
|
||||
queryset=VRF.objects.all(),
|
||||
label='VRF',
|
||||
)
|
||||
# Duplicate of `vrf` for backward-compatibility
|
||||
vrf_id = django_filters.MethodFilter(
|
||||
action='_vrf',
|
||||
label='VRF',
|
||||
vrf = NullableModelMultipleChoiceFilter(
|
||||
name='vrf',
|
||||
queryset=VRF.objects.all(),
|
||||
to_field_name='rd',
|
||||
label='VRF (RD)',
|
||||
)
|
||||
tenant_id = django_filters.MethodFilter(
|
||||
action='_tenant_id',
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.MethodFilter(
|
||||
action='_tenant',
|
||||
label='Tenant',
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='interface__device',
|
||||
@ -232,7 +232,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
@ -253,35 +253,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
except AddrFormatError:
|
||||
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):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -317,12 +288,12 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=VLANGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=VLANGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
@ -337,23 +308,23 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
name='vid',
|
||||
label='VLAN number (1-4095)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
tenant = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
role_id = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
role = NullableModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
to_field_name='slug',
|
||||
|
@ -5,7 +5,9 @@ from dcim.models import Site, Device, Interface
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
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 (
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
model = VRF
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
|
||||
null_option=(0, None))
|
||||
|
||||
|
||||
#
|
||||
@ -128,16 +125,11 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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):
|
||||
model = Aggregate
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||
rir = forms.MultipleChoiceField(required=False, choices=aggregate_rir_choices, label='RIR',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug',
|
||||
label='RIR')
|
||||
|
||||
|
||||
#
|
||||
@ -268,21 +260,6 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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():
|
||||
status_counts = {}
|
||||
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]
|
||||
|
||||
|
||||
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):
|
||||
model = Prefix
|
||||
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Network',
|
||||
}))
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd',
|
||||
label='VRF', null_option=(0, 'Global'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
model = IPAddress
|
||||
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Prefix',
|
||||
}))
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='rd',
|
||||
label='VRF', null_option=(0, 'Global'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
||||
to_field_name='slug', null_option=(0, 'None'))
|
||||
|
||||
|
||||
#
|
||||
@ -470,14 +436,8 @@ class VLANGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
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):
|
||||
site = forms.MultipleChoiceField(required=False, choices=vlangroup_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug')
|
||||
|
||||
|
||||
#
|
||||
@ -555,21 +515,6 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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():
|
||||
status_counts = {}
|
||||
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]
|
||||
|
||||
|
||||
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):
|
||||
model = VLAN
|
||||
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
|
||||
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
|
||||
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
|
||||
null_option=(0, 'None'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
||||
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
|
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 tenancy.models import Tenant
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
from utilities.sql import NullsFirstQuerySet
|
||||
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
|
||||
@ -192,7 +193,7 @@ class Role(models.Model):
|
||||
return self.vlans.count()
|
||||
|
||||
|
||||
class PrefixQuerySet(models.QuerySet):
|
||||
class PrefixQuerySet(NullsFirstQuerySet):
|
||||
|
||||
def annotate_depth(self, limit=None):
|
||||
"""
|
||||
@ -249,7 +250,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
objects = PrefixQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['family', 'prefix']
|
||||
ordering = ['vrf', 'family', 'prefix']
|
||||
verbose_name_plural = 'prefixes'
|
||||
|
||||
def __unicode__(self):
|
||||
|
@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.6.0'
|
||||
VERSION = '1.6.1'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
@ -71,7 +71,7 @@ if LDAP_CONFIGURED:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
except ImportError:
|
||||
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__)))
|
||||
|
||||
|
@ -25,7 +25,7 @@ $(document).ready(function() {
|
||||
});
|
||||
if (slug_field) {
|
||||
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')) {
|
||||
slug_field.val(slugify($(this).val(), 50));
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
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
|
||||
|
||||
@ -95,13 +95,8 @@ class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
||||
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):
|
||||
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>
|
||||
</table>
|
||||
</div>
|
||||
{% with circuit.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=circuit %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
@ -105,7 +105,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with provider.custom_fields as custom_fields %}
|
||||
{% with provider.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
<div class="panel panel-default">
|
||||
|
@ -144,7 +144,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with device.custom_fields as custom_fields %}
|
||||
{% with device.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% if request.user.is_authenticated %}
|
||||
|
@ -132,7 +132,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with rack.custom_fields as custom_fields %}
|
||||
{% with rack.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
<div class="panel panel-default">
|
||||
|
@ -111,7 +111,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with site.custom_fields as custom_fields %}
|
||||
{% with site.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
<div class="panel panel-default">
|
||||
|
@ -82,7 +82,7 @@
|
||||
{% include 'inc/created_updated.html' with obj=aggregate %}
|
||||
</div>
|
||||
<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' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
@ -121,6 +121,9 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with ipaddress.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
@ -101,6 +101,9 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with prefix.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=prefix %}
|
||||
<br />
|
||||
</div>
|
||||
|
@ -6,6 +6,15 @@
|
||||
|
||||
{% block content %}
|
||||
<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 %}
|
||||
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
|
@ -110,6 +110,9 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with vlan.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=vlan %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
@ -82,6 +82,9 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with vrf.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
@ -65,7 +65,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with tenant.custom_fields as custom_fields %}
|
||||
{% with tenant.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
<div class="panel panel-default">
|
||||
|
@ -3,6 +3,7 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@ -11,12 +12,12 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
|
@ -2,7 +2,7 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
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
|
||||
|
||||
@ -74,12 +74,7 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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):
|
||||
model = Tenant
|
||||
group = forms.MultipleChoiceField(required=False, choices=tenant_group_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
group = FilterChoiceField(queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
|
||||
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 itertools
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
@ -142,10 +143,14 @@ class CSVDataField(forms.CharField):
|
||||
if not self.help_text:
|
||||
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):
|
||||
# Return a list of dictionaries, each representing an individual record
|
||||
records = []
|
||||
reader = csv.reader(value.splitlines())
|
||||
reader = csv.reader(self.utf_8_encoder(value.splitlines()))
|
||||
for i, row in enumerate(reader, start=1):
|
||||
if row:
|
||||
if len(row) < len(self.columns):
|
||||
@ -222,6 +227,32 @@ class SlugField(forms.SlugField):
|
||||
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
|
||||
#
|
||||
|
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.contrib import messages
|
||||
@ -10,18 +11,30 @@ from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, TypedCho
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template import TemplateSyntaxError
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import is_safe_url
|
||||
from django.views.generic import View
|
||||
|
||||
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 .forms import ConfirmationForm
|
||||
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):
|
||||
queryset = None
|
||||
filter = None
|
||||
@ -39,19 +52,26 @@ class ObjectListView(View):
|
||||
if self.filter:
|
||||
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
|
||||
if 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:
|
||||
response = et.to_response(context_dict={'queryset': self.queryset.all()},
|
||||
filename='netbox_{}'.format(self.queryset.model._meta.verbose_name_plural))
|
||||
response = et.to_response(context_dict={'queryset': queryset},
|
||||
filename='netbox_{}'.format(model._meta.verbose_name_plural))
|
||||
return response
|
||||
except TemplateSyntaxError:
|
||||
messages.error(request, "There was an error rendering the selected export template ({})."
|
||||
.format(et.name))
|
||||
# Fall back to built-in CSV export
|
||||
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(
|
||||
output,
|
||||
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