mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-28 03:16:25 -06:00
Merge remote-tracking branch 'upstream/develop' into multipoint_circuits
This commit is contained in:
commit
bbc428359d
@ -1,4 +1,4 @@
|
|||||||
This section entails features of NetBox which are not crucial to its primary functions, but that provide additional value.
|
This section entails features of NetBox which are not crucial to its primary functions, but provide additional value.
|
||||||
|
|
||||||
# Custom Fields
|
# Custom Fields
|
||||||
|
|
||||||
@ -17,15 +17,15 @@ Custom fields must be created through the admin UI under Extras > Custom Fields.
|
|||||||
|
|
||||||
Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form.
|
Assign the field a name. This should be a simple database-friendly string, e.g. `tps_report`. You may optionally assign the field a human-friendly label (e.g. "TPS report") as well; the label will be displayed on forms. If a description is provided, it will appear beneath the field in a form.
|
||||||
|
|
||||||
Marking the field as required will force the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.)
|
Marking the field as required will require the user to provide a value for the field when creating a new object or when saving an existing object. A default value for the field may also be provided. Use "true" or "false" for boolean fields. (The default value has no effect for selection fields.)
|
||||||
|
|
||||||
When creating a selection field, you must create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically.
|
When creating a selection field, you should create at least two choices. These choices will be arranged first by weight, with lower weights appearing higher in the list, and then alphabetically.
|
||||||
|
|
||||||
## Using Custom Fields
|
## Using Custom Fields
|
||||||
|
|
||||||
When a single object is edited, the form will include any custom fields which have been defined for its type. These fields are included in the "Custom Fields" panel. Each custom field value must be saved independently from the core object, so it's best to avoid adding too many custom fields per object.
|
When a single object is edited, the form will include any custom fields which have been defined for the object type. These fields are included in the "Custom Fields" panel. On the backend, each custom field value is saved separately from the core object as an independent database call, so it's best to avoid adding too many custom fields per object.
|
||||||
|
|
||||||
When editing multiple objects, values are saved in bulk per field. That is, there is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
|
When editing multiple objects, custom field values are saved in bulk. There is no significant difference in overhead when saving a custom field value for 100 objects versus one object. However, the bulk operation must be performed separately for each custom field.
|
||||||
|
|
||||||
# Export Templates
|
# Export Templates
|
||||||
|
|
||||||
|
@ -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, get_filter_choices,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Circuit, CircuitType, Provider, Termination
|
from .models import Circuit, CircuitType, Provider, Termination
|
||||||
@ -64,8 +65,7 @@ def provider_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(choices=get_filter_choices(Site, id_field='slug'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -85,6 +85,19 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||||
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||||
|
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
|
||||||
|
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
|
attrs={'filter-for': 'device'}))
|
||||||
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||||
|
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||||
|
attrs={'filter-for': 'interface'}))
|
||||||
|
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||||
|
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||||
|
)
|
||||||
|
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
|
||||||
|
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||||
|
disabled_indicator='is_connected'))
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -95,126 +108,15 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
help_texts = {
|
help_texts = {
|
||||||
'cid': "Unique circuit ID",
|
'cid': "Unique circuit ID",
|
||||||
'install_date': "Format: YYYY-MM-DD",
|
'install_date': "Format: YYYY-MM-DD",
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
|
|
||||||
super(CircuitForm, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitFromCSVForm(forms.ModelForm):
|
|
||||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
|
||||||
error_messages={'invalid_choice': 'Provider not found.'})
|
|
||||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
|
||||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Circuit
|
|
||||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date']
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
|
||||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
|
||||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
|
||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
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}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Terminations
|
|
||||||
#
|
|
||||||
class TerminationForm(BootstrapMixin, CustomFieldForm):
|
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
|
||||||
rack = forms.ModelChoiceField(
|
|
||||||
queryset=Rack.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label='Rack',
|
|
||||||
widget=APISelect(
|
|
||||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
|
||||||
attrs={'filter-for': 'device'}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
device = forms.ModelChoiceField(
|
|
||||||
queryset=Device.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label='Device',
|
|
||||||
widget=APISelect(
|
|
||||||
api_url='/api/dcim/devices/?rack_id={{rack}}',
|
|
||||||
attrs={'filter-for': 'interface'}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
livesearch = forms.CharField(
|
|
||||||
required=False,
|
|
||||||
label='Device',
|
|
||||||
widget=Livesearch(
|
|
||||||
query_key='q',
|
|
||||||
query_url='dcim-api:device_list',
|
|
||||||
field_to_update='device'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
interface = forms.ModelChoiceField(
|
|
||||||
queryset=Interface.objects.all(),
|
|
||||||
required=False,
|
|
||||||
label='Interface',
|
|
||||||
widget=APISelect(
|
|
||||||
api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
|
||||||
disabled_indicator='is_connected'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
comments = CommentField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Termination
|
|
||||||
fields = [
|
|
||||||
'tid', 'site', 'rack', 'device', 'livesearch',
|
|
||||||
'interface', 'port_speed', 'upstream_speed', 'commit_rate',
|
|
||||||
'xconnect_id', 'pp_info', 'comments'
|
|
||||||
]
|
|
||||||
help_texts = {
|
|
||||||
'tid': "Termination ID",
|
|
||||||
'port_speed': "Physical circuit speed",
|
'port_speed': "Physical circuit speed",
|
||||||
'commit_rate': "Commited rate",
|
'commit_rate': "Commited rate",
|
||||||
'xconnect_id': "ID of the local cross-connect",
|
'xconnect_id': "ID of the local cross-connect",
|
||||||
'pp_info': "Patch panel ID and port number(s)"
|
'pp_info': "Patch panel ID and port number(s)"
|
||||||
}
|
}
|
||||||
widgets = {
|
|
||||||
'circuit': forms.HiddenInput(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
super(TerminationForm, self).__init__(*args, **kwargs)
|
super(CircuitForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# If this circuit has been assigned to an interface, initialize rack and device
|
# If this circuit has been assigned to an interface, initialize rack and device
|
||||||
if self.instance.interface:
|
if self.instance.interface:
|
||||||
@ -240,11 +142,11 @@ class TerminationForm(BootstrapMixin, CustomFieldForm):
|
|||||||
# Limit interface choices
|
# Limit interface choices
|
||||||
if self.is_bound and self.data.get('device'):
|
if self.is_bound and self.data.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.data['device'])\
|
interfaces = Interface.objects.filter(device=self.data['device'])\
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
|
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||||
elif self.initial.get('device'):
|
elif self.initial.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
||||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('termination', 'connected_as_a', 'connected_as_b')
|
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||||
else:
|
else:
|
||||||
interfaces = []
|
interfaces = []
|
||||||
@ -254,3 +156,40 @@ class TerminationForm(BootstrapMixin, CustomFieldForm):
|
|||||||
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
||||||
}) for iface in interfaces
|
}) for iface in interfaces
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitFromCSVForm(forms.ModelForm):
|
||||||
|
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Provider not found.'})
|
||||||
|
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
||||||
|
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||||
|
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Site not found.'})
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Circuit
|
||||||
|
fields = ['cid', 'provider', 'type', 'tenant', 'install_date']
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
||||||
|
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||||
|
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||||
|
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||||
|
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
||||||
|
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||||
|
comments = CommentField()
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
|
model = Circuit
|
||||||
|
type = FilterChoiceField(choices=get_filter_choices(CircuitType, id_field='slug', count_field='circuits'))
|
||||||
|
provider = FilterChoiceField(choices=get_filter_choices(Provider, id_field='slug', count_field='circuits'))
|
||||||
|
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='circuits'))
|
||||||
|
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='circuits'))
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
@ -9,7 +9,8 @@ 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,
|
||||||
|
get_filter_choices
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -117,15 +118,9 @@ 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(choices=get_filter_choices(Tenant, id_field='slug', count_field='sites'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -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(choices=get_filter_choices(Site, id_field='slug', count_field='rack_groups'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -254,36 +243,13 @@ 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(choices=get_filter_choices(Site, id_field='slug', count_field='racks'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
group_id = FilterChoiceField(choices=get_filter_choices(RackGroup, select_related=['site'], count_field='racks'),
|
||||||
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
|
label='Rack Group')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='racks'))
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
|
role = FilterChoiceField(choices=get_filter_choices(RackRole, id_field='slug', count_field='racks'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
|
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -317,14 +283,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(choices=get_filter_choices(Manufacturer, id_field='slug',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
count_field='device_types'))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -627,49 +588,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(choices=get_filter_choices(Site, id_field='slug', count_field='racks__devices'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
rack_group_id = FilterChoiceField(choices=get_filter_choices(RackGroup, select_related=['site'],
|
||||||
rack_group_id = forms.MultipleChoiceField(required=False, choices=device_rack_group_choices, label='Rack Group',
|
count_field='racks__devices'),
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
label='Rack Group')
|
||||||
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
role = FilterChoiceField(choices=get_filter_choices(DeviceRole, id_field='slug', count_field='devices'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='devices'))
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices,
|
device_type_id = FilterChoiceField(choices=get_filter_choices(DeviceType, select_related=['manufacturer'],
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
count_field='instances'),
|
||||||
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
|
label='Type')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
platform = FilterChoiceField(choices=get_filter_choices(Platform, id_field='slug', count_field='devices'))
|
||||||
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))
|
||||||
|
|
||||||
|
|
||||||
|
25
netbox/dcim/migrations/0019_new_iface_form_factors.py
Normal file
25
netbox/dcim/migrations/0019_new_iface_form_factors.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10 on 2016-09-13 15:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0018_device_add_asset_tag'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
]
|
@ -77,23 +77,39 @@ ROLE_COLOR_CHOICES = [
|
|||||||
[COLOR_GRAY3, 'Dark Gray'],
|
[COLOR_GRAY3, 'Dark Gray'],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Virtual
|
||||||
IFACE_FF_VIRTUAL = 0
|
IFACE_FF_VIRTUAL = 0
|
||||||
IFACE_FF_100M_COPPER = 800
|
# Ethernet
|
||||||
IFACE_FF_1GE_COPPER = 1000
|
IFACE_FF_100ME_FIXED = 800
|
||||||
IFACE_FF_GBIC = 1050
|
IFACE_FF_1GE_FIXED = 1000
|
||||||
IFACE_FF_SFP = 1100
|
IFACE_FF_1GE_GBIC = 1050
|
||||||
IFACE_FF_10GE_COPPER = 1150
|
IFACE_FF_1GE_SFP = 1100
|
||||||
IFACE_FF_SFP_PLUS = 1200
|
IFACE_FF_10GE_FIXED = 1150
|
||||||
IFACE_FF_XFP = 1300
|
IFACE_FF_10GE_SFP_PLUS = 1200
|
||||||
IFACE_FF_QSFP_PLUS = 1400
|
IFACE_FF_10GE_XFP = 1300
|
||||||
IFACE_FF_CFP = 1500
|
IFACE_FF_10GE_XENPAK = 1310
|
||||||
IFACE_FF_QSFP28 = 1600
|
IFACE_FF_10GE_X2 = 1320
|
||||||
|
IFACE_FF_25GE_SFP28 = 1350
|
||||||
|
IFACE_FF_40GE_QSFP_PLUS = 1400
|
||||||
|
IFACE_FF_100GE_CFP = 1500
|
||||||
|
IFACE_FF_100GE_QSFP28 = 1600
|
||||||
|
# Fibrechannel
|
||||||
|
IFACE_FF_1GFC_SFP = 3010
|
||||||
|
IFACE_FF_2GFC_SFP = 3020
|
||||||
|
IFACE_FF_4GFC_SFP = 3040
|
||||||
|
IFACE_FF_8GFC_SFP_PLUS = 3080
|
||||||
|
IFACE_FF_16GFC_SFP_PLUS = 3160
|
||||||
|
# Serial
|
||||||
IFACE_FF_T1 = 4000
|
IFACE_FF_T1 = 4000
|
||||||
IFACE_FF_E1 = 4010
|
IFACE_FF_E1 = 4010
|
||||||
IFACE_FF_T3 = 4040
|
IFACE_FF_T3 = 4040
|
||||||
IFACE_FF_E3 = 4050
|
IFACE_FF_E3 = 4050
|
||||||
|
# Stacking
|
||||||
IFACE_FF_STACKWISE = 5000
|
IFACE_FF_STACKWISE = 5000
|
||||||
IFACE_FF_STACKWISE_PLUS = 5050
|
IFACE_FF_STACKWISE_PLUS = 5050
|
||||||
|
# Other
|
||||||
|
IFACE_FF_OTHER = 32767
|
||||||
|
|
||||||
IFACE_FF_CHOICES = [
|
IFACE_FF_CHOICES = [
|
||||||
[
|
[
|
||||||
'Virtual interfaces',
|
'Virtual interfaces',
|
||||||
@ -102,23 +118,36 @@ IFACE_FF_CHOICES = [
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Ethernet',
|
'Ethernet (fixed)',
|
||||||
[
|
[
|
||||||
[IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'],
|
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
|
||||||
[IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'],
|
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
|
||||||
[IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'],
|
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Modular',
|
'Ethernet (modular)',
|
||||||
[
|
[
|
||||||
[IFACE_FF_GBIC, 'GBIC (1GE)'],
|
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
|
||||||
[IFACE_FF_SFP, 'SFP (1GE)'],
|
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
|
||||||
[IFACE_FF_XFP, 'XFP (10GE)'],
|
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
|
||||||
[IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'],
|
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
|
||||||
[IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'],
|
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
|
||||||
[IFACE_FF_CFP, 'CFP (100GE)'],
|
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
|
||||||
[IFACE_FF_QSFP28, 'QSFP28 (100GE)'],
|
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
|
||||||
|
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||||
|
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
|
||||||
|
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'FibreChannel',
|
||||||
|
[
|
||||||
|
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
|
||||||
|
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
|
||||||
|
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
|
||||||
|
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
|
||||||
|
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -137,6 +166,12 @@ IFACE_FF_CHOICES = [
|
|||||||
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'Other',
|
||||||
|
[
|
||||||
|
[IFACE_FF_OTHER, 'Other'],
|
||||||
|
]
|
||||||
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
STATUS_ACTIVE = True
|
STATUS_ACTIVE = True
|
||||||
@ -647,7 +682,7 @@ class InterfaceTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
|
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||||
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
||||||
|
|
||||||
objects = InterfaceTemplateManager()
|
objects = InterfaceTemplateManager()
|
||||||
@ -1023,7 +1058,7 @@ class Interface(models.Model):
|
|||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||||
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
||||||
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management',
|
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management',
|
||||||
help_text="This interface is used only for out-of-band management")
|
help_text="This interface is used only for out-of-band management")
|
||||||
|
@ -29,8 +29,8 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
elif cf.type == CF_TYPE_BOOLEAN:
|
elif cf.type == CF_TYPE_BOOLEAN:
|
||||||
choices = (
|
choices = (
|
||||||
(None, '---------'),
|
(None, '---------'),
|
||||||
(True, 'True'),
|
(1, 'True'),
|
||||||
(False, 'False'),
|
(0, 'False'),
|
||||||
)
|
)
|
||||||
if cf.default.lower() in ['true', 'yes', '1']:
|
if cf.default.lower() in ['true', 'yes', '1']:
|
||||||
initial = True
|
initial = True
|
||||||
|
@ -234,10 +234,10 @@ class ExportTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
template = Template(self.template_code)
|
template = Template(self.template_code)
|
||||||
mime_type = 'text/plain' if not self.mime_type else self.mime_type
|
mime_type = 'text/plain' if not self.mime_type else self.mime_type
|
||||||
response = HttpResponse(
|
output = template.render(Context(context_dict))
|
||||||
template.render(Context(context_dict)),
|
# Replace CRLF-style line terminators
|
||||||
content_type=mime_type
|
output = output.replace('\r\n', '\n')
|
||||||
)
|
response = HttpResponse(output, content_type=mime_type)
|
||||||
if self.file_extension:
|
if self.file_extension:
|
||||||
filename += '.{}'.format(self.file_extension)
|
filename += '.{}'.format(self.file_extension)
|
||||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||||
|
@ -5,7 +5,10 @@ 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,
|
||||||
|
get_filter_choices,
|
||||||
|
)
|
||||||
|
|
||||||
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,
|
||||||
@ -14,6 +17,11 @@ from .models import (
|
|||||||
|
|
||||||
FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
|
FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
|
||||||
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
|
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
|
||||||
|
IP_FAMILY_CHOICES = [
|
||||||
|
('', 'All'),
|
||||||
|
(4, 'IPv4'),
|
||||||
|
(6, 'IPv6'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def bulkedit_vrf_choices():
|
def bulkedit_vrf_choices():
|
||||||
@ -64,15 +72,9 @@ 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(choices=get_filter_choices(Tenant, id_field='slug', count_field='vrfs'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -123,15 +125,10 @@ 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
|
||||||
rir = forms.MultipleChoiceField(required=False, choices=aggregate_rir_choices, label='RIR',
|
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
rir = FilterChoiceField(choices=get_filter_choices(RIR, id_field='slug', count_field='aggregates'), label='RIR')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -262,21 +259,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'):
|
||||||
@ -284,26 +266,18 @@ 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',
|
||||||
}))
|
}))
|
||||||
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
vrf = FilterChoiceField(choices=get_filter_choices(VRF, count_field='prefixes'), label='VRF')
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='prefixes'),
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
label='Tenant')
|
||||||
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
|
status = FilterChoiceField(choices=prefix_status_choices)
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='prefixes'))
|
||||||
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
|
role = FilterChoiceField(choices=get_filter_choices(Role, id_field='slug', count_field='prefixes'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
|
||||||
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
|
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
|
||||||
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
||||||
|
|
||||||
|
|
||||||
@ -434,25 +408,15 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
def ipaddress_family_choices():
|
|
||||||
return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')]
|
|
||||||
|
|
||||||
|
|
||||||
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=ipaddress_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(choices=get_filter_choices(VRF, count_field='ip_addresses'), label='VRF')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='prefixes'),
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
label='Tenant')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -467,14 +431,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(choices=get_filter_choices(Site, id_field='slug', count_field='vlan_groups'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -552,21 +510,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'):
|
||||||
@ -574,19 +517,11 @@ 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(choices=get_filter_choices(Site, id_field='slug', count_field='vlans'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
group_id = FilterChoiceField(choices=get_filter_choices(VLANGroup, select_related=['site'], count_field='vlans'),
|
||||||
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
|
label='VLAN Group')
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='vlans'))
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices,
|
status = FilterChoiceField(choices=vlan_status_choices)
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
role = FilterChoiceField(choices=get_filter_choices(Role, id_field='slug', count_field='vlans'))
|
||||||
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
|
|
||||||
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
|
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.5.3-dev'
|
VERSION = '1.6.1-dev'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
|
@ -34,7 +34,8 @@ body {
|
|||||||
footer p {
|
footer p {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
@media (max-width: 1120px) {
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
.navbar-header {
|
.navbar-header {
|
||||||
float: none;
|
float: none;
|
||||||
}
|
}
|
||||||
@ -54,6 +55,7 @@ footer p {
|
|||||||
}
|
}
|
||||||
.navbar-collapse.collapse {
|
.navbar-collapse.collapse {
|
||||||
display: none!important;
|
display: none!important;
|
||||||
|
max-height: none;
|
||||||
}
|
}
|
||||||
.navbar-nav {
|
.navbar-nav {
|
||||||
float: none!important;
|
float: none!important;
|
||||||
@ -84,13 +86,11 @@ th.pk, td.pk {
|
|||||||
width: 30px;
|
width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Paginator */
|
/* Paginator */
|
||||||
nav ul.pagination {
|
nav ul.pagination {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Racks */
|
/* Racks */
|
||||||
div.rack_header {
|
div.rack_header {
|
||||||
margin-left: 36px;
|
margin-left: 36px;
|
||||||
|
@ -32,61 +32,6 @@ $(document).ready(function() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper select fields
|
|
||||||
$('select.helper-parent').change(function () {
|
|
||||||
|
|
||||||
// Resolve child field by ID specified in parent
|
|
||||||
var child_field = $('#id_' + $(this).attr('child'));
|
|
||||||
|
|
||||||
// Wipe out any existing options within the child field
|
|
||||||
child_field.empty();
|
|
||||||
child_field.append($("<option></option>").attr("value", "").text(""));
|
|
||||||
|
|
||||||
// If the parent has a value set, fetch a list of child options via the API and populate the child field with them
|
|
||||||
if ($(this).val()) {
|
|
||||||
|
|
||||||
// Construct the API request URL
|
|
||||||
var api_url = $(this).attr('child-source');
|
|
||||||
var parent_accessor = $(this).attr('parent-accessor');
|
|
||||||
if (parent_accessor) {
|
|
||||||
api_url += '?' + parent_accessor + '=' + $(this).val();
|
|
||||||
} else {
|
|
||||||
api_url += '?' + $(this).attr('name') + '_id=' + $(this).val();
|
|
||||||
}
|
|
||||||
var api_url_extra = $(this).attr('child-filter');
|
|
||||||
if (api_url_extra) {
|
|
||||||
api_url += '&' + api_url_extra;
|
|
||||||
}
|
|
||||||
|
|
||||||
var disabled_indicator = $(this).attr('disabled-indicator');
|
|
||||||
var disabled_exempt = child_field.attr('exempt');
|
|
||||||
var child_display = $(this).attr('child-display');
|
|
||||||
if (!child_display) {
|
|
||||||
child_display = 'name';
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: api_url,
|
|
||||||
dataType: 'json',
|
|
||||||
success: function (response, status) {
|
|
||||||
console.log(response);
|
|
||||||
$.each(response, function (index, choice) {
|
|
||||||
var option = $("<option></option>").attr("value", choice.id).text(choice[child_display]);
|
|
||||||
if (disabled_indicator && choice[disabled_indicator] && choice.id != disabled_exempt) {
|
|
||||||
option.attr("disabled", "disabled")
|
|
||||||
}
|
|
||||||
child_field.append(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger change event in case the child field is the parent of another field
|
|
||||||
child_field.change();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// API select widget
|
// API select widget
|
||||||
$('select[filter-for]').change(function () {
|
$('select[filter-for]').change(function () {
|
||||||
|
|
||||||
|
@ -2,10 +2,11 @@ from Crypto.Cipher import PKCS1_OAEP
|
|||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
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, get_filter_choices,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
@ -95,13 +96,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(choices=get_filter_choices(SecretRole, id_field='slug', count_field='secrets'))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -167,7 +167,7 @@
|
|||||||
{% if perms.ipam.add_rir or perms.ipam.add_role %}
|
{% if perms.ipam.add_rir or perms.ipam.add_role %}
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
|
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix Roles</a></li>
|
||||||
{% if perms.ipam.add_role %}
|
{% if perms.ipam.add_role %}
|
||||||
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
|
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -186,6 +186,11 @@
|
|||||||
{% if perms.ipam.add_vlangroup %}
|
{% if perms.ipam.add_vlangroup %}
|
||||||
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
|
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Roles</a></li>
|
||||||
|
{% if perms.ipam.add_role %}
|
||||||
|
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Face</td>
|
<td>Face</td>
|
||||||
<td>Rack face; front or rear (optional)</td>
|
<td>Rack face; front or rear (required if position is set)</td>
|
||||||
<td>Rear</td>
|
<td>Rear</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
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, get_filter_choices,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
@ -74,12 +75,6 @@ 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(choices=get_filter_choices(TenantGroup, id_field='slug', count_field='tenants'))
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
||||||
|
@ -3,6 +3,7 @@ import re
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
|
from django.db.models import Count
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@ -34,6 +35,27 @@ def add_blank_choice(choices):
|
|||||||
return ((None, '---------'),) + choices
|
return ((None, '---------'),) + choices
|
||||||
|
|
||||||
|
|
||||||
|
def get_filter_choices(model, id_field='pk', select_related=[], count_field=None):
|
||||||
|
"""
|
||||||
|
Return a list of choices suitable for a ChoiceField.
|
||||||
|
|
||||||
|
:param model: The base model to use for the queryset
|
||||||
|
:param id_field: Field to use as the object identifier
|
||||||
|
:param select_related: Any related tables to include
|
||||||
|
:param count: The field to use for a child COUNT() (optional)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
queryset = model.objects.all()
|
||||||
|
if select_related:
|
||||||
|
queryset = queryset.select_related(*select_related)
|
||||||
|
if count_field:
|
||||||
|
queryset = queryset.annotate(child_count=Count(count_field))
|
||||||
|
return [(getattr(obj, id_field), u'{} ({})'.format(obj, obj.child_count)) for obj in queryset]
|
||||||
|
else:
|
||||||
|
return [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Widgets
|
# Widgets
|
||||||
#
|
#
|
||||||
@ -222,6 +244,16 @@ class SlugField(forms.SlugField):
|
|||||||
self.widget.attrs['slug-source'] = slug_source
|
self.widget.attrs['slug-source'] = slug_source
|
||||||
|
|
||||||
|
|
||||||
|
class FilterChoiceField(forms.MultipleChoiceField):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Forms
|
# Forms
|
||||||
#
|
#
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
@ -381,10 +380,6 @@ class BulkDeleteView(View):
|
|||||||
template_name = 'utilities/confirm_bulk_delete.html'
|
template_name = 'utilities/confirm_bulk_delete.html'
|
||||||
default_redirect_url = None
|
default_redirect_url = None
|
||||||
|
|
||||||
@method_decorator(staff_member_required)
|
|
||||||
def dispatch(self, *args, **kwargs):
|
|
||||||
return super(BulkDeleteView, self).dispatch(*args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
# Attempt to derive parent object if a parent class has been given
|
# Attempt to derive parent object if a parent class has been given
|
||||||
|
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