mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 03:56:53 -06:00
commit
c171547037
@ -3,7 +3,6 @@ from django.db.models import Count
|
|||||||
|
|
||||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
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, FilterChoiceField, Livesearch, SmallTextarea,
|
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
|
||||||
@ -57,6 +56,9 @@ class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
|
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Provider
|
model = Provider
|
||||||
@ -86,7 +88,7 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
attrs={'filter-for': 'device'}))
|
attrs={'filter-for': 'device'}))
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||||
attrs={'filter-for': 'interface'}))
|
display_field='display_name', attrs={'filter-for': 'interface'}))
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||||
)
|
)
|
||||||
@ -178,11 +180,14 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||||
provider = forms.ModelChoiceField(queryset=Provider.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')
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
||||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, 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
|
||||||
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, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
||||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
||||||
|
SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||||
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole,
|
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES,
|
||||||
Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -42,37 +43,12 @@ def get_device_by_name_or_pk(name):
|
|||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
def bulkedit_platform_choices():
|
def validate_connection_status(value):
|
||||||
choices = [
|
|
||||||
(None, '---------'),
|
|
||||||
(0, 'None'),
|
|
||||||
]
|
|
||||||
choices += [(p.pk, p.name) for p in Platform.objects.all()]
|
|
||||||
return choices
|
|
||||||
|
|
||||||
|
|
||||||
def bulkedit_rackgroup_choices():
|
|
||||||
"""
|
"""
|
||||||
Include an option to remove the currently assigned group from a rack.
|
Custom validator for connection statuses. value must be either "planned" or "connected" (case-insensitive).
|
||||||
"""
|
"""
|
||||||
choices = [
|
if value.lower() not in ['planned', 'connected']:
|
||||||
(None, '---------'),
|
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
||||||
(0, 'None'),
|
|
||||||
]
|
|
||||||
choices += [(r.pk, r) for r in RackGroup.objects.all()]
|
|
||||||
return choices
|
|
||||||
|
|
||||||
|
|
||||||
def bulkedit_rackrole_choices():
|
|
||||||
"""
|
|
||||||
Include an option to remove the currently assigned role from a rack.
|
|
||||||
"""
|
|
||||||
choices = [
|
|
||||||
(None, '---------'),
|
|
||||||
(0, 'None'),
|
|
||||||
]
|
|
||||||
choices += [(r.pk, r.name) for r in RackRole.objects.all()]
|
|
||||||
return choices
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -114,7 +90,10 @@ class SiteImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
|
|
||||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['tenant']
|
||||||
|
|
||||||
|
|
||||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
@ -234,14 +213,17 @@ class RackImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
|
||||||
group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group')
|
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group')
|
||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role')
|
role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False)
|
||||||
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
|
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
|
||||||
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
|
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
|
||||||
u_height = forms.IntegerField(required=False, label='Height (U)')
|
u_height = forms.IntegerField(required=False, label='Height (U)')
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['group', 'tenant', 'role', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Rack
|
model = Rack
|
||||||
@ -279,7 +261,7 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
|
|||||||
'is_pdu', 'is_network_device', 'subdevice_role']
|
'is_pdu', 'is_network_device', 'subdevice_role']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||||
u_height = forms.IntegerField(min_value=1, required=False)
|
u_height = forms.IntegerField(min_value=1, required=False)
|
||||||
@ -334,6 +316,14 @@ class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name_pattern', 'form_factor', 'mgmt_only']
|
fields = ['name_pattern', 'form_factor', 'mgmt_only']
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
|
class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
@ -583,12 +573,14 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
||||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
|
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
|
||||||
label='Platform')
|
|
||||||
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['tenant', 'platform']
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Device
|
model = Device
|
||||||
@ -631,7 +623,7 @@ class ConsoleConnectionCSVForm(forms.Form):
|
|||||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Device not found'})
|
error_messages={'invalid_choice': 'Device not found'})
|
||||||
console_port = forms.CharField()
|
console_port = forms.CharField()
|
||||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
status = forms.CharField(validators=[validate_connection_status])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
@ -695,6 +687,7 @@ class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
|
|||||||
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
||||||
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
|
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
|
||||||
|
display_field='display_name',
|
||||||
attrs={'filter-for': 'cs_port'}))
|
attrs={'filter-for': 'cs_port'}))
|
||||||
livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
|
livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
|
query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
|
||||||
@ -762,7 +755,7 @@ class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
|
|||||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||||
attrs={'filter-for': 'port'}))
|
display_field='display_name', attrs={'filter-for': 'port'}))
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||||
)
|
)
|
||||||
@ -826,7 +819,7 @@ class PowerConnectionCSVForm(forms.Form):
|
|||||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Device not found'})
|
error_messages={'invalid_choice': 'Device not found'})
|
||||||
power_port = forms.CharField()
|
power_port = forms.CharField()
|
||||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
status = forms.CharField(validators=[validate_connection_status])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
@ -891,7 +884,7 @@ class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
|
|||||||
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
||||||
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
|
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
|
||||||
attrs={'filter-for': 'power_outlet'}))
|
display_field='display_name', attrs={'filter-for': 'power_outlet'}))
|
||||||
livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
|
livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
|
query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
|
||||||
)
|
)
|
||||||
@ -958,7 +951,7 @@ class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
|
|||||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||||
attrs={'filter-for': 'port'}))
|
display_field='display_name', attrs={'filter-for': 'port'}))
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||||
)
|
)
|
||||||
@ -1023,6 +1016,15 @@ class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
|||||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||||
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['description']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interface connections
|
# Interface connections
|
||||||
#
|
#
|
||||||
@ -1033,6 +1035,7 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
|||||||
widget=forms.Select(attrs={'filter-for': 'device_b'}))
|
widget=forms.Select(attrs={'filter-for': 'device_b'}))
|
||||||
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
||||||
|
display_field='display_name',
|
||||||
attrs={'filter-for': 'interface_b'}))
|
attrs={'filter-for': 'interface_b'}))
|
||||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
|
query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
|
||||||
@ -1087,7 +1090,7 @@ class InterfaceConnectionCSVForm(forms.Form):
|
|||||||
device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Device B not found.'})
|
error_messages={'invalid_choice': 'Device B not found.'})
|
||||||
interface_b = forms.CharField()
|
interface_b = forms.CharField()
|
||||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
status = forms.CharField(validators=[validate_connection_status])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
@ -3,10 +3,6 @@ from django.conf.urls import url
|
|||||||
from secrets.views import secret_add
|
from secrets.views import secret_add
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
from .models import (
|
|
||||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, PowerPortTemplate, PowerOutletTemplate,
|
|
||||||
InterfaceTemplate,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -75,6 +71,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Interface templates
|
# Interface templates
|
||||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
|
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
|
||||||
|
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||||
|
|
||||||
# Device bay templates
|
# Device bay templates
|
||||||
@ -159,6 +156,7 @@ urlpatterns = [
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_add_multi'),
|
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_add_multi'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||||
|
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||||
|
@ -457,6 +457,14 @@ class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
|||||||
form = forms.InterfaceTemplateForm
|
form = forms.InterfaceTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'dcim.change_interfacetemplate'
|
||||||
|
cls = InterfaceTemplate
|
||||||
|
parent_cls = DeviceType
|
||||||
|
form = forms.InterfaceTemplateBulkEditForm
|
||||||
|
template_name = 'dcim/interfacetemplate_bulk_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_interfacetemplate'
|
permission_required = 'dcim.delete_interfacetemplate'
|
||||||
cls = InterfaceTemplate
|
cls = InterfaceTemplate
|
||||||
@ -1425,6 +1433,14 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
|||||||
len(selected_devices)))
|
len(selected_devices)))
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'dcim.change_interface'
|
||||||
|
cls = Interface
|
||||||
|
parent_cls = Device
|
||||||
|
form = forms.InterfaceBulkEditForm
|
||||||
|
template_name = 'dcim/interface_bulk_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_interface'
|
permission_required = 'dcim.delete_interface'
|
||||||
cls = Interface
|
cls = Interface
|
||||||
|
@ -3,7 +3,7 @@ from collections import OrderedDict
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from utilities.forms import LaxURLField
|
from utilities.forms import BulkEditForm, LaxURLField
|
||||||
from .models import (
|
from .models import (
|
||||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
|
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
|
||||||
)
|
)
|
||||||
@ -49,8 +49,6 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
# Select
|
# Select
|
||||||
elif cf.type == CF_TYPE_SELECT:
|
elif cf.type == CF_TYPE_SELECT:
|
||||||
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
|
||||||
if not cf.required:
|
|
||||||
choices = [(0, 'None')] + choices
|
|
||||||
if bulk_edit or filterable_only:
|
if bulk_edit or filterable_only:
|
||||||
choices = [(None, '---------')] + choices
|
choices = [(None, '---------')] + choices
|
||||||
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required)
|
||||||
@ -73,10 +71,10 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
|
|
||||||
|
|
||||||
class CustomFieldForm(forms.ModelForm):
|
class CustomFieldForm(forms.ModelForm):
|
||||||
custom_fields = []
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
self.custom_fields = []
|
||||||
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||||
|
|
||||||
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
||||||
@ -126,22 +124,25 @@ class CustomFieldForm(forms.ModelForm):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldBulkEditForm(forms.Form):
|
class CustomFieldBulkEditForm(BulkEditForm):
|
||||||
custom_fields = []
|
|
||||||
|
|
||||||
def __init__(self, model, *args, **kwargs):
|
|
||||||
|
|
||||||
self.obj_type = ContentType.objects.get_for_model(model)
|
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
super(CustomFieldBulkEditForm, self).__init__(*args, **kwargs)
|
super(CustomFieldBulkEditForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.custom_fields = []
|
||||||
|
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||||
|
|
||||||
# Add all applicable CustomFields to the form
|
# Add all applicable CustomFields to the form
|
||||||
custom_fields = []
|
custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items()
|
||||||
for name, field in get_custom_fields_for_model(self.obj_type, bulk_edit=True).items():
|
for name, field in custom_fields:
|
||||||
|
# Annotate non-required custom fields as nullable
|
||||||
|
if not field.required:
|
||||||
|
self.nullable_fields.append(name)
|
||||||
field.required = False
|
field.required = False
|
||||||
self.fields[name] = field
|
self.fields[name] = field
|
||||||
custom_fields.append(name)
|
# Annotate this as a custom field
|
||||||
self.custom_fields = custom_fields
|
self.custom_fields.append(name)
|
||||||
|
print(self.nullable_fields)
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilterForm(forms.Form):
|
class CustomFieldFilterForm(forms.Form):
|
||||||
|
@ -3,7 +3,6 @@ from django.db.models import Count
|
|||||||
|
|
||||||
from dcim.models import Site, Device, Interface
|
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.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField,
|
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField,
|
||||||
@ -23,18 +22,6 @@ IP_FAMILY_CHOICES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def bulkedit_vrf_choices():
|
|
||||||
"""
|
|
||||||
Include an option to assign the object to the global table.
|
|
||||||
"""
|
|
||||||
choices = [
|
|
||||||
(None, '---------'),
|
|
||||||
(0, 'Global'),
|
|
||||||
]
|
|
||||||
choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
|
||||||
return choices
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
@ -67,9 +54,12 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
|
|
||||||
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['tenant', 'description']
|
||||||
|
|
||||||
|
|
||||||
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = VRF
|
model = VRF
|
||||||
@ -124,6 +114,9 @@ class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
date_added = forms.DateField(required=False)
|
date_added = forms.DateField(required=False)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['date_added', 'description']
|
||||||
|
|
||||||
|
|
||||||
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
@ -253,12 +246,15 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||||
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
|
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description']
|
||||||
|
|
||||||
|
|
||||||
def prefix_status_choices():
|
def prefix_status_choices():
|
||||||
status_counts = {}
|
status_counts = {}
|
||||||
@ -294,6 +290,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
|||||||
widget=forms.Select(attrs={'filter-for': 'nat_device'}))
|
widget=forms.Select(attrs={'filter-for': 'nat_device'}))
|
||||||
nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}',
|
widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}',
|
||||||
|
display_field='display_name',
|
||||||
attrs={'filter-for': 'nat_inside'}))
|
attrs={'filter-for': 'nat_inside'}))
|
||||||
livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch(
|
livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch(
|
||||||
query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address')
|
query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address')
|
||||||
@ -407,10 +404,13 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
|
|
||||||
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['vrf', 'tenant', 'description']
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@ -509,11 +509,14 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
|
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
|
||||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['group', 'tenant', 'role', 'description']
|
||||||
|
|
||||||
|
|
||||||
def vlan_status_choices():
|
def vlan_status_choices():
|
||||||
status_counts = {}
|
status_counts = {}
|
||||||
|
@ -139,7 +139,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
if self.pk:
|
if self.pk:
|
||||||
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
covered_aggregates = covered_aggregates.exclude(pk=self.pk)
|
||||||
if covered_aggregates:
|
if covered_aggregates:
|
||||||
raise ValidationError("{} is overlaps with an existing aggregate ({})"
|
raise ValidationError("{} overlaps with an existing aggregate ({})"
|
||||||
.format(self.prefix, covered_aggregates[0]))
|
.format(self.prefix, covered_aggregates[0]))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
@ -9,7 +9,7 @@ import os
|
|||||||
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
||||||
#
|
#
|
||||||
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
||||||
ALLOWED_HOSTS = [os.environ.get('ALLOWED_HOSTS', '')]
|
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(' ')
|
||||||
|
|
||||||
# PostgreSQL database configuration.
|
# PostgreSQL database configuration.
|
||||||
DATABASE = {
|
DATABASE = {
|
||||||
|
@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.6.2-r1'
|
VERSION = '1.6.3'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
@ -162,7 +162,7 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||||
STATIC_ROOT = BASE_DIR + '/static/'
|
STATIC_ROOT = BASE_DIR + '/static/'
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/{}static/'.format(BASE_PATH)
|
||||||
STATICFILES_DIRS = (
|
STATICFILES_DIRS = (
|
||||||
os.path.join(BASE_DIR, "project-static"),
|
os.path.join(BASE_DIR, "project-static"),
|
||||||
)
|
)
|
||||||
@ -176,8 +176,7 @@ MESSAGE_TAGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Authentication URLs
|
# Authentication URLs
|
||||||
LOGIN_URL = '/login/'
|
LOGIN_URL = '/{}login/'.format(BASE_PATH)
|
||||||
LOGIN_REDIRECT_URL = '/'
|
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
SECRETS_MIN_PUBKEY_SIZE = 2048
|
SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
// "Toggle all" checkbox in a table header
|
// "Toggle all" checkbox (table header)
|
||||||
$('#toggle_all').click(function (event) {
|
$('#toggle_all').click(function (event) {
|
||||||
$('td input:checkbox[name=pk]').prop('checked', $(this).prop('checked'));
|
$('td input:checkbox[name=pk]').prop('checked', $(this).prop('checked'));
|
||||||
if ($(this).is(':checked')) {
|
if ($(this).is(':checked')) {
|
||||||
@ -16,6 +16,15 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Simple "Toggle all" button (panel)
|
||||||
|
$('button.toggle').click(function (event) {
|
||||||
|
var selected = $(this).attr('selected');
|
||||||
|
$(this).closest('form').find('input:checkbox[name=pk]').prop('checked', !selected);
|
||||||
|
$(this).attr('selected', !selected);
|
||||||
|
$(this).children('span').toggleClass('glyphicon-unchecked glyphicon-check');
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
// Slugify
|
// Slugify
|
||||||
function slugify(s, num_chars) {
|
function slugify(s, num_chars) {
|
||||||
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
||||||
@ -37,6 +46,11 @@ $(document).ready(function() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bulk edit nullification
|
||||||
|
$('input:checkbox[name=_nullify]').click(function (event) {
|
||||||
|
$('#id_' + this.value).toggle('disabled');
|
||||||
|
});
|
||||||
|
|
||||||
// API select widget
|
// API select widget
|
||||||
$('select[filter-for]').change(function () {
|
$('select[filter-for]').change(function () {
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from django import forms
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
||||||
|
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
@ -89,11 +89,14 @@ class SecretImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'}))
|
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'}))
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
class SecretBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
||||||
name = forms.CharField(max_length=100, required=False)
|
name = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class SecretFilterForm(forms.Form, BootstrapMixin):
|
class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||||
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
||||||
|
@ -314,13 +314,16 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Device Bays</strong>
|
<strong>Device Bays</strong>
|
||||||
{% if perms.dcim.add_devicebay and device_bays|length > 10 %}
|
<div class="pull-right">
|
||||||
<div class="pull-right">
|
<button class="btn btn-default btn-xs toggle">
|
||||||
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% if perms.dcim.add_devicebay and device_bays|length > 10 %}
|
||||||
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for devicebay in device_bays %}
|
{% for devicebay in device_bays %}
|
||||||
@ -355,19 +358,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces or device.device_type.is_network_device %}
|
{% if interfaces or device.device_type.is_network_device %}
|
||||||
{% if perms.dcim.delete_interface %}
|
{% if perms.dcim.delete_interface %}
|
||||||
<form method="post" action="{% url 'dcim:interface_bulk_delete' pk=device.pk %}">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Interfaces</strong>
|
<strong>Interfaces</strong>
|
||||||
{% if perms.dcim.add_interface and interfaces|length > 10 %}
|
<div class="pull-right">
|
||||||
<div class="pull-right">
|
<button class="btn btn-default btn-xs toggle">
|
||||||
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% if perms.dcim.add_interface and interfaces|length > 10 %}
|
||||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for iface in interfaces %}
|
{% for iface in interfaces %}
|
||||||
@ -380,8 +386,13 @@
|
|||||||
</table>
|
</table>
|
||||||
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
|
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
|
{% if interfaces and perms.dcim.change_interface %}
|
||||||
|
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}" class="btn btn-warning btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.delete_interface %}
|
{% if interfaces and perms.dcim.delete_interface %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -408,13 +419,16 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Console Server Ports</strong>
|
<strong>Console Server Ports</strong>
|
||||||
{% if perms.dcim.add_consoleserverport and cs_ports|length > 10 %}
|
<div class="pull-right">
|
||||||
<div class="pull-right">
|
<button class="btn btn-default btn-xs toggle">
|
||||||
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% if perms.dcim.add_consoleserverport and cs_ports|length > 10 %}
|
||||||
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for csp in cs_ports %}
|
{% for csp in cs_ports %}
|
||||||
@ -455,13 +469,16 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Power Outlets</strong>
|
<strong>Power Outlets</strong>
|
||||||
{% if perms.dcim.add_poweroutlet and power_outlets|length > 10 %}
|
<div class="pull-right">
|
||||||
<div class="pull-right">
|
<button class="btn btn-default btn-xs toggle">
|
||||||
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% if perms.dcim.add_poweroutlet and power_outlets|length > 10 %}
|
||||||
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
{% for po in power_outlets %}
|
{% for po in power_outlets %}
|
||||||
|
@ -72,6 +72,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Instances</td>
|
||||||
|
<td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@ -143,14 +147,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' delete_url='dcim:devicetype_delete_interface' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% if devicetype.is_parent_device %}
|
{% if devicetype.is_parent_device %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if devicetype.is_network_device %}
|
{% if devicetype.is_network_device %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' delete_url='dcim:devicetype_delete_interface' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if devicetype.is_console_server %}
|
{% if devicetype.is_console_server %}
|
||||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
|
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
|
||||||
|
@ -1,25 +1,37 @@
|
|||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
{% if perms.dcim.change_devicetype %}
|
{% if perms.dcim.change_devicetype %}
|
||||||
<form method="post" action="{% url delete_url pk=devicetype.pk %}">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>{{ title }}</strong>
|
<strong>{{ title }}</strong>
|
||||||
{% if table.rows|length > 10 %}
|
<div class="pull-right">
|
||||||
<div class="pull-right">
|
{% if table.rows|length > 3 %}
|
||||||
|
<button class="btn btn-default btn-xs toggle">
|
||||||
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if table.rows|length > 10 %}
|
||||||
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs">
|
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
Add {{ title }}
|
Add {{ title }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% render_table table 'table.html' %}
|
{% render_table table 'table.html' %}
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
{% if table.rows %}
|
{% if table.rows %}
|
||||||
<button type="submit" class="btn btn-xs btn-danger">
|
{% if edit_url %}
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}" class="btn btn-xs btn-warning">
|
||||||
</button>
|
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if delete_url %}
|
||||||
|
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}" class="btn btn-xs btn-danger">
|
||||||
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs">
|
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs">
|
||||||
|
17
netbox/templates/dcim/interface_bulk_edit.html
Normal file
17
netbox/templates/dcim/interface_bulk_edit.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'utilities/bulk_edit_form.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Interface Bulk Edit{% endblock %}
|
||||||
|
|
||||||
|
{% block selected_objects_table %}
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Form Factor</th>
|
||||||
|
</tr>
|
||||||
|
{% for iface in selected_objects %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ iface.name }}</td>
|
||||||
|
<td>{{ iface.get_form_factor_display }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
25
netbox/templates/dcim/interfacetemplate_bulk_edit.html
Normal file
25
netbox/templates/dcim/interfacetemplate_bulk_edit.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'utilities/bulk_edit_form.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Interface Template Bulk Edit{% endblock %}
|
||||||
|
|
||||||
|
{% block selected_objects_table %}
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Form Factor</th>
|
||||||
|
<th>Management</th>
|
||||||
|
</tr>
|
||||||
|
{% for iface in selected_objects %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ iface.name }}</td>
|
||||||
|
<td>{{ iface.get_form_factor_display }}</td>
|
||||||
|
<td>
|
||||||
|
{% if iface.mgmt_only %}
|
||||||
|
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
@ -8,13 +8,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ field }}</td>
|
<td>{{ field }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if value == True %}
|
{% if field.type == 300 and value == True %}
|
||||||
<i class="glyphicon glyphicon-ok text-success" title="True"></i>
|
<i class="glyphicon glyphicon-ok text-success" title="True"></i>
|
||||||
{% elif value == False %}
|
{% elif field.type == 300 and value == False %}
|
||||||
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
|
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
|
||||||
{% elif field.type == 500 and value %}
|
{% elif field.type == 500 and value %}
|
||||||
{{ value|urlizetrunc:75 }}
|
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
|
||||||
{% elif value %}
|
{% elif field.type == 200 or value %}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{% elif field.required %}
|
{% elif field.required %}
|
||||||
<span class="text-warning">Not defined</span>
|
<span class="text-warning">Not defined</span>
|
||||||
|
@ -8,6 +8,9 @@
|
|||||||
{% if request.POST.redirect_url %}
|
{% if request.POST.redirect_url %}
|
||||||
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
|
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@ -29,7 +32,13 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>{% block form_title %}Attributes{% endblock %}</strong></div>
|
<div class="panel-heading"><strong>{% block form_title %}Attributes{% endblock %}</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_form form %}
|
{% for field in form.visible_fields %}
|
||||||
|
{% if field.name in form.nullable_fields %}
|
||||||
|
{% render_field field bulk_nullable=True %}
|
||||||
|
{% else %}
|
||||||
|
{% render_field field %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group text-right">
|
<div class="form-group text-right">
|
||||||
|
@ -5,26 +5,26 @@
|
|||||||
<div class="col-md-9 col-md-offset-3">
|
<div class="col-md-9 col-md-offset-3">
|
||||||
<div class="checkbox{% if field.errors %} has-error{% endif %}">
|
<div class="checkbox{% if field.errors %} has-error{% endif %}">
|
||||||
<label for="{{ field.id_for_label }}">
|
<label for="{{ field.id_for_label }}">
|
||||||
{{ field }}
|
{{ field }} {{ field.label }}
|
||||||
{{ field.label }}
|
|
||||||
</label>
|
</label>
|
||||||
{% if field.help_text %}
|
{% if field.help_text %}
|
||||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% if bulk_nullable %}
|
||||||
{% elif field|widget_type == 'radioselect' %}
|
<label class="checkbox-inline">
|
||||||
<div class="col-md-9 col-md-offset-3">
|
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||||
<div class="radio{% if field.errors %} has-error{% endif %}">
|
|
||||||
<label for="{{ field.id_for_label }}">
|
|
||||||
{{ field }}
|
|
||||||
{{ field.label }}
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% elif field|widget_type == 'textarea' %}
|
{% elif field|widget_type == 'textarea' %}
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{{ field }}
|
{{ field }}
|
||||||
|
{% if bulk_nullable %}
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
{% if field.help_text %}
|
{% if field.help_text %}
|
||||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -40,6 +40,11 @@
|
|||||||
<label class="col-md-3 control-label{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
<label class="col-md-3 control-label{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
{{ field }}
|
{{ field }}
|
||||||
|
{% if bulk_nullable %}
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
<input type="checkbox" name="_nullify" value="{{ field.name }}" /> Set null
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
{% if field.help_text %}
|
{% if field.help_text %}
|
||||||
<span class="help-block">{{ field.help_text|safe }}</span>
|
<span class="help-block">{{ field.help_text|safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -7,30 +7,6 @@ from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDat
|
|||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
def bulkedit_tenantgroup_choices():
|
|
||||||
"""
|
|
||||||
Include an option to remove the currently assigned TenantGroup from a Tenant.
|
|
||||||
"""
|
|
||||||
choices = [
|
|
||||||
(None, '---------'),
|
|
||||||
(0, 'None'),
|
|
||||||
]
|
|
||||||
choices += [(g.pk, g.name) for g in TenantGroup.objects.all()]
|
|
||||||
return choices
|
|
||||||
|
|
||||||
|
|
||||||
def bulkedit_tenant_choices():
|
|
||||||
"""
|
|
||||||
Include an option to remove the currently assigned Tenant from an object.
|
|
||||||
"""
|
|
||||||
choices = [
|
|
||||||
(None, '---------'),
|
|
||||||
(0, 'None'),
|
|
||||||
]
|
|
||||||
choices += [(t.pk, t.name) for t in Tenant.objects.all()]
|
|
||||||
return choices
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tenant groups
|
# Tenant groups
|
||||||
#
|
#
|
||||||
@ -71,7 +47,10 @@ class TenantImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
|
|
||||||
class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group')
|
group = forms.ModelChoiceField(queryset=TenantGroup.objects.all(), required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ['group']
|
||||||
|
|
||||||
|
|
||||||
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect, render, resolve_url
|
from django.shortcuts import redirect, render
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
|
|
||||||
from secrets.forms import UserKeyForm
|
from secrets.forms import UserKeyForm
|
||||||
@ -26,7 +25,7 @@ def login(request):
|
|||||||
# Determine where to direct user after successful login
|
# Determine where to direct user after successful login
|
||||||
redirect_to = request.POST.get('next', '')
|
redirect_to = request.POST.get('next', '')
|
||||||
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
||||||
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
|
redirect_to = reverse('home')
|
||||||
|
|
||||||
# Authenticate user
|
# Authenticate user
|
||||||
auth_login(request, form.get_user())
|
auth_login(request, form.get_user())
|
||||||
|
@ -34,7 +34,7 @@ def add_blank_choice(choices):
|
|||||||
"""
|
"""
|
||||||
Add a blank choice to the beginning of a choices list.
|
Add a blank choice to the beginning of a choices list.
|
||||||
"""
|
"""
|
||||||
return ((None, '---------'),) + choices
|
return ((None, '---------'),) + tuple(choices)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -294,6 +294,18 @@ class ConfirmationForm(forms.Form, BootstrapMixin):
|
|||||||
confirm = forms.BooleanField(required=True)
|
confirm = forms.BooleanField(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkEditForm(forms.Form):
|
||||||
|
|
||||||
|
def __init__(self, model, *args, **kwargs):
|
||||||
|
super(BulkEditForm, self).__init__(*args, **kwargs)
|
||||||
|
self.model = model
|
||||||
|
# Copy any nullable fields defined in Meta
|
||||||
|
if hasattr(self.Meta, 'nullable_fields'):
|
||||||
|
self.nullable_fields = [field for field in self.Meta.nullable_fields]
|
||||||
|
else:
|
||||||
|
self.nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
class BulkImportForm(forms.Form):
|
class BulkImportForm(forms.Form):
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -12,4 +12,4 @@ class LoginRequiredMiddleware:
|
|||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
if LOGIN_REQUIRED and not request.user.is_authenticated():
|
if LOGIN_REQUIRED and not request.user.is_authenticated():
|
||||||
if request.path_info != settings.LOGIN_URL:
|
if request.path_info != settings.LOGIN_URL:
|
||||||
return HttpResponseRedirect(settings.LOGIN_URL)
|
return HttpResponseRedirect('{}?next={}'.format(settings.LOGIN_URL, request.path_info))
|
||||||
|
@ -5,12 +5,13 @@ register = template.Library()
|
|||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/render_field.html')
|
@register.inclusion_tag('utilities/render_field.html')
|
||||||
def render_field(field):
|
def render_field(field, bulk_nullable=False):
|
||||||
"""
|
"""
|
||||||
Render a single form field from template
|
Render a single form field from template
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'field': field,
|
'field': field,
|
||||||
|
'bulk_nullable': bulk_nullable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -280,42 +280,59 @@ class BulkImportView(View):
|
|||||||
|
|
||||||
class BulkEditView(View):
|
class BulkEditView(View):
|
||||||
cls = None
|
cls = None
|
||||||
|
parent_cls = None
|
||||||
form = None
|
form = None
|
||||||
template_name = None
|
template_name = None
|
||||||
default_redirect_url = None
|
default_redirect_url = None
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self):
|
||||||
return redirect(self.default_redirect_url)
|
return redirect(self.default_redirect_url)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, **kwargs):
|
||||||
|
|
||||||
|
# Attempt to derive parent object if a parent class has been given
|
||||||
|
if self.parent_cls:
|
||||||
|
parent_obj = get_object_or_404(self.parent_cls, **kwargs)
|
||||||
|
else:
|
||||||
|
parent_obj = None
|
||||||
|
|
||||||
|
# Determine URL to redirect users upon modification of objects
|
||||||
posted_redirect_url = request.POST.get('redirect_url')
|
posted_redirect_url = request.POST.get('redirect_url')
|
||||||
if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()):
|
if posted_redirect_url and is_safe_url(url=posted_redirect_url, host=request.get_host()):
|
||||||
redirect_url = posted_redirect_url
|
redirect_url = posted_redirect_url
|
||||||
else:
|
elif parent_obj:
|
||||||
|
redirect_url = parent_obj.get_absolute_url()
|
||||||
|
elif self.default_redirect_url:
|
||||||
redirect_url = reverse(self.default_redirect_url)
|
redirect_url = reverse(self.default_redirect_url)
|
||||||
|
else:
|
||||||
|
raise ImproperlyConfigured('No redirect URL has been provided.')
|
||||||
|
|
||||||
|
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||||
if request.POST.get('_all'):
|
if request.POST.get('_all'):
|
||||||
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||||
else:
|
else:
|
||||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||||
|
|
||||||
if '_apply' in request.POST:
|
if '_apply' in request.POST:
|
||||||
if hasattr(self.form, 'custom_fields'):
|
form = self.form(self.cls, request.POST)
|
||||||
form = self.form(self.cls, request.POST)
|
|
||||||
else:
|
|
||||||
form = self.form(request.POST)
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
|
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
|
||||||
standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk']
|
standard_fields = [field for field in form.fields if field not in custom_fields and field != 'pk']
|
||||||
|
|
||||||
# Update objects
|
# Update standard fields. If a field is listed in _nullify, delete its value.
|
||||||
updated_count = self.update_objects(pk_list, form, standard_fields)
|
nullified_fields = request.POST.getlist('_nullify')
|
||||||
|
fields_to_update = {}
|
||||||
|
for field in standard_fields:
|
||||||
|
if field in form.nullable_fields and field in nullified_fields:
|
||||||
|
fields_to_update[field] = ''
|
||||||
|
elif form.cleaned_data[field]:
|
||||||
|
fields_to_update[field] = form.cleaned_data[field]
|
||||||
|
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||||
|
|
||||||
# Update custom fields for objects
|
# Update custom fields for objects
|
||||||
if custom_fields:
|
if custom_fields:
|
||||||
objs_updated = self.update_custom_fields(pk_list, form, custom_fields)
|
objs_updated = self.update_custom_fields(pk_list, form, custom_fields, nullified_fields)
|
||||||
if objs_updated and not updated_count:
|
if objs_updated and not updated_count:
|
||||||
updated_count = objs_updated
|
updated_count = objs_updated
|
||||||
|
|
||||||
@ -326,10 +343,7 @@ class BulkEditView(View):
|
|||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if hasattr(self.form, 'custom_fields'):
|
form = self.form(self.cls, initial={'pk': pk_list})
|
||||||
form = self.form(self.cls, initial={'pk': pk_list})
|
|
||||||
else:
|
|
||||||
form = self.form(initial={'pk': pk_list})
|
|
||||||
|
|
||||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||||
if not selected_objects:
|
if not selected_objects:
|
||||||
@ -342,26 +356,23 @@ class BulkEditView(View):
|
|||||||
'cancel_url': redirect_url,
|
'cancel_url': redirect_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
def update_objects(self, pk_list, form, fields):
|
def update_custom_fields(self, pk_list, form, fields, nullified_fields):
|
||||||
fields_to_update = {}
|
|
||||||
|
|
||||||
for name in fields:
|
|
||||||
# Check for zero value (bulk editing)
|
|
||||||
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
|
|
||||||
fields_to_update[name] = None
|
|
||||||
elif form.cleaned_data[name]:
|
|
||||||
fields_to_update[name] = form.cleaned_data[name]
|
|
||||||
|
|
||||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
|
||||||
|
|
||||||
def update_custom_fields(self, pk_list, form, fields):
|
|
||||||
obj_type = ContentType.objects.get_for_model(self.cls)
|
obj_type = ContentType.objects.get_for_model(self.cls)
|
||||||
objs_updated = False
|
objs_updated = False
|
||||||
|
|
||||||
for name in fields:
|
for name in fields:
|
||||||
if form.cleaned_data[name] not in [None, u'']:
|
|
||||||
|
|
||||||
field = form.fields[name].model
|
field = form.fields[name].model
|
||||||
|
|
||||||
|
# Setting the field to null
|
||||||
|
if name in form.nullable_fields and name in nullified_fields:
|
||||||
|
|
||||||
|
# Delete all CustomFieldValues for instances of this field belonging to the selected objects.
|
||||||
|
CustomFieldValue.objects.filter(field=field, obj_type=obj_type, obj_id__in=pk_list).delete()
|
||||||
|
objs_updated = True
|
||||||
|
|
||||||
|
# Updating the value of the field
|
||||||
|
elif form.cleaned_data[name] not in [None, u'']:
|
||||||
|
|
||||||
# Check for zero value (bulk editing)
|
# Check for zero value (bulk editing)
|
||||||
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
|
if isinstance(form.fields[name], TypedChoiceField) and form.cleaned_data[name] == 0:
|
||||||
@ -400,7 +411,7 @@ 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
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, **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
|
||||||
if self.parent_cls:
|
if self.parent_cls:
|
||||||
@ -421,9 +432,9 @@ class BulkDeleteView(View):
|
|||||||
|
|
||||||
# Are we deleting *all* objects in the queryset or just a selected subset?
|
# Are we deleting *all* objects in the queryset or just a selected subset?
|
||||||
if request.POST.get('_all'):
|
if request.POST.get('_all'):
|
||||||
pk_list = [x for x in request.POST.get('pk_all').split(',') if x]
|
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||||
else:
|
else:
|
||||||
pk_list = request.POST.getlist('pk')
|
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||||
|
|
||||||
form_cls = self.get_form()
|
form_cls = self.get_form()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user