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

Merge branch 'develop' into develop-2.10

This commit is contained in:
Jeremy Stretch
2020-11-05 15:33:07 -05:00
26 changed files with 1534 additions and 169 deletions

23
.github/lock.yml vendored
View File

@ -1,23 +0,0 @@
# Configuration for Lock (https://github.com/apps/lock)
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 90
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: false
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true
# Limit to only `issues` or `pulls`
# only: issues

21
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,21 @@
# lock-threads (https://github.com/marketplace/actions/lock-threads)
name: 'Lock threads'
on:
schedule:
- cron: '0 3 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '90'
issue-exclude-created-before: ''
issue-exclude-labels: ''
issue-lock-labels: ''
issue-lock-comment: ''
issue-lock-reason: 'resolved'
process-only: 'issues'

View File

@ -73,8 +73,9 @@ tar -xf netbox_media.tar.gz
## Cache Invalidation
If you are migrating your instance of NetBox to a different machine, be sure to first invalidate the cache by performing this command:
If you are migrating your instance of NetBox to a different machine, be sure to first invalidate the cache on the original instance by issuing the `invalidate all` management command (within the Python virtual environment):
```no-highlight
python3 manage.py invalidate all
# source /opt/netbox/venv/bin/activate
(venv) # python3 manage.py invalidate all
```

View File

@ -1,5 +1,19 @@
# NetBox v2.9
## v2.9.9 (FUTURE)
### Enhancements
* [#5304](https://github.com/netbox-community/netbox/issues/5304) - Return server error messages as JSON when handling REST API requests
* [#5310](https://github.com/netbox-community/netbox/issues/5310) - Link to rack groups within rack list table
### Bug Fixes
* [#5271](https://github.com/netbox-community/netbox/issues/5271) - Fix auto-population of region field when editing a device
---
## v2.9.8 (2020-10-30)
### Enhancements

View File

@ -303,14 +303,24 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
#
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all()
queryset=Site.objects.all(),
query_params={
'region_id': '$region'
}
)
class Meta:
model = CircuitTermination
fields = [
'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
'term_side', 'region', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
]
help_texts = {
'port_speed': "Physical circuit speed",

View File

@ -352,8 +352,18 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
#
class RackGroupForm(BootstrapMixin, forms.ModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all()
queryset=Site.objects.all(),
query_params={
'region_id': '$region'
}
)
parent = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
@ -367,7 +377,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = RackGroup
fields = (
'site', 'parent', 'name', 'slug', 'description',
'region', 'site', 'parent', 'name', 'slug', 'description',
)
@ -447,14 +457,17 @@ class RackRoleCSVForm(CSVModelForm):
#
class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
site = DynamicModelChoiceField(
queryset=Site.objects.all()
)
group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
query_params={
'site_id': '$site'
'region_id': '$region'
}
)
role = DynamicModelChoiceField(
@ -470,8 +483,9 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class Meta:
model = Rack
fields = [
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag',
'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags',
'region', 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
'comments', 'tags',
]
help_texts = {
'site': "The site at which the rack exists",
@ -548,9 +562,19 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
queryset=Rack.objects.all(),
widget=forms.MultipleHiddenInput
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region_id': '$region'
}
)
group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
@ -691,9 +715,19 @@ class RackElevationFilterForm(RackFilterForm):
#
class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region_id': '$region'
}
)
rack_group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
@ -707,7 +741,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
display_field='display_name',
query_params={
'site_id': '$site',
'group_id': 'rack',
'group_id': '$rack',
}
)
units = NumericArrayField(
@ -809,15 +843,23 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditFor
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
model = RackReservation
field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
field_order = ['q', 'region', 'site', 'group_id', 'tenant_group', 'tenant']
q = forms.CharField(
required=False,
label='Search'
)
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False
required=False,
query_params={
'region': '$region'
}
)
group_id = DynamicModelMultipleChoiceField(
queryset=RackGroup.objects.prefetch_related('site'),
@ -1672,7 +1714,10 @@ class PlatformCSVForm(CSVModelForm):
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
@ -1686,6 +1731,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
display_field='display_name',
query_params={
'site_id': '$site'
},
initial_params={
'racks': '$rack'
}
)
rack = DynamicModelChoiceField(
@ -1711,7 +1759,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
)
manufacturer = DynamicModelChoiceField(
queryset=Manufacturer.objects.all(),
required=False
required=False,
initial_params={
'device_types': '$device_type'
}
)
device_type = DynamicModelChoiceField(
queryset=DeviceType.objects.all(),
@ -1733,7 +1784,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
cluster_group = DynamicModelChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
null_option='None'
null_option='None',
initial_params={
'clusters': '$cluster'
}
)
cluster = DynamicModelChoiceField(
queryset=Cluster.objects.all(),
@ -1772,27 +1826,6 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
}
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
if 'initial' not in kwargs:
kwargs['initial'] = {}
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
if instance and hasattr(instance, 'device_type'):
kwargs['initial']['manufacturer'] = instance.device_type.manufacturer
if instance and instance.cluster is not None:
kwargs['initial']['cluster_group'] = instance.cluster.group
if 'device_type' in kwargs['initial'] and 'manufacturer' not in kwargs['initial']:
device_type_id = kwargs['initial']['device_type']
manufacturer_id = DeviceType.objects.filter(pk=device_type_id).values_list('manufacturer__pk', flat=True).first()
kwargs['initial']['manufacturer'] = manufacturer_id
if 'cluster' in kwargs['initial'] and 'cluster_group' not in kwargs['initial']:
cluster_id = kwargs['initial']['cluster']
cluster_group_id = Cluster.objects.filter(pk=cluster_id).values_list('group__pk', flat=True).first()
kwargs['initial']['cluster_group'] = cluster_group_id
super().__init__(*args, **kwargs)
if self.instance.pk:
@ -3441,10 +3474,18 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm):
"""
Base form for connecting a Cable to a Device component
"""
termination_b_region = DynamicModelChoiceField(
queryset=Region.objects.all(),
label='Region',
required=False
)
termination_b_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False
required=False,
query_params={
'region_id': '$termination_b_region'
}
)
termination_b_rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
@ -3470,8 +3511,8 @@ class ConnectCableToDeviceForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = Cable
fields = [
'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
'label', 'color', 'length', 'length_unit',
'termination_b_region', 'termination_b_site', 'termination_b_rack', 'termination_b_device',
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit',
]
widgets = {
'status': StaticSelect2,
@ -3568,10 +3609,18 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
label='Provider',
required=False
)
termination_b_region = DynamicModelChoiceField(
queryset=Region.objects.all(),
label='Region',
required=False
)
termination_b_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False
required=False,
query_params={
'region_id': '$termination_b_region'
}
)
termination_b_circuit = DynamicModelChoiceField(
queryset=Circuit.objects.all(),
@ -3595,8 +3644,8 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = Cable
fields = [
'termination_b_provider', 'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type',
'status', 'label', 'color', 'length', 'length_unit',
'termination_b_provider', 'termination_b_region', 'termination_b_site', 'termination_b_circuit',
'termination_b_id', 'type', 'status', 'label', 'color', 'length', 'length_unit',
]
def clean_termination_b_id(self):
@ -3605,11 +3654,18 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, forms.ModelForm):
class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
termination_b_region = DynamicModelChoiceField(
queryset=Region.objects.all(),
label='Region',
required=False
)
termination_b_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
label='Site',
required=False,
display_field='cid'
query_params={
'region_id': '$termination_b_region'
}
)
termination_b_rackgroup = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
@ -3851,10 +3907,18 @@ class CableFilterForm(BootstrapMixin, forms.Form):
required=False,
label='Search'
)
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False
required=False,
query_params={
'region': '$region'
}
)
tenant = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
@ -3903,10 +3967,18 @@ class CableFilterForm(BootstrapMixin, forms.Form):
#
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False
required=False,
query_params={
'region': '$region'
}
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@ -3919,10 +3991,18 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False
required=False,
query_params={
'region': '$region'
}
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@ -3935,10 +4015,18 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False
required=False,
query_params={
'region': '$region'
}
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@ -3962,9 +4050,19 @@ class DeviceSelectionForm(forms.Form):
class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region_id': '$region'
}
)
rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
@ -3997,7 +4095,7 @@ class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
class Meta:
model = VirtualChassis
fields = [
'name', 'domain', 'site', 'rack', 'members', 'initial_position', 'tags',
'name', 'domain', 'region', 'site', 'rack', 'members', 'initial_position', 'tags',
]
def save(self, *args, **kwargs):
@ -4094,9 +4192,19 @@ class DeviceVCMembershipForm(forms.ModelForm):
class VCMemberSelectForm(BootstrapMixin, forms.Form):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region_id': '$region'
}
)
rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
@ -4195,8 +4303,18 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
#
class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all()
queryset=Site.objects.all(),
query_params={
'region_id': '$region'
}
)
rack_group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
@ -4213,7 +4331,7 @@ class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
class Meta:
model = PowerPanel
fields = [
'site', 'rack_group', 'name', 'tags',
'region', 'site', 'rack_group', 'name', 'tags',
]
@ -4248,9 +4366,19 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
queryset=PowerPanel.objects.all(),
widget=forms.MultipleHiddenInput
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region_id': '$region'
}
)
rack_group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
@ -4302,9 +4430,22 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
#
class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites__powerpanel': '$power_panel'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
initial_params={
'powerpanel': '$power_panel'
},
query_params={
'region_id': '$region'
}
)
power_panel = DynamicModelChoiceField(
queryset=PowerPanel.objects.all(),
@ -4329,7 +4470,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
class Meta:
model = PowerFeed
fields = [
'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
'region', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'comments', 'tags',
]
widgets = {
@ -4339,14 +4480,6 @@ class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
'phase': StaticSelect2(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Initialize site field
if self.instance and hasattr(self.instance, 'power_panel'):
self.initial['site'] = self.instance.power_panel.site
class PowerFeedCSVForm(CustomFieldModelCSVForm):
site = CSVModelChoiceField(

1100
netbox/dcim/tables.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -70,12 +70,15 @@ class RackRoleTable(BaseTable):
class RackTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(
order_by=('_name',)
name = tables.Column(
order_by=('_name',),
linkify=True
)
site = tables.LinkColumn(
viewname='dcim:site',
args=[Accessor('site__slug')]
group = tables.Column(
linkify=True
)
site = tables.Column(
linkify=True
)
tenant = tables.TemplateColumn(
template_code=COL_TENANT

View File

@ -100,23 +100,6 @@ class DeviceTestCase(TestCase):
self.assertIn('face', form.errors)
self.assertIn('position', form.errors)
def test_initial_data_population(self):
device_type = DeviceType.objects.first()
cluster = Cluster.objects.first()
test = DeviceForm(initial={
'device_type': device_type.pk,
'device_role': DeviceRole.objects.first().pk,
'status': DeviceStatusChoices.STATUS_ACTIVE,
'site': Site.objects.first().pk,
'cluster': cluster.pk,
})
# Check that the initial value for the manufacturer is set automatically when assigning the device type
self.assertEqual(test.initial['manufacturer'], device_type.manufacturer.pk)
# Check that the initial value for the cluster group is set automatically when assigning the cluster
self.assertEqual(test.initial['cluster_group'], cluster.group.pk)
class LabelTestCase(TestCase):

View File

@ -2089,8 +2089,11 @@ class CableCreateView(ObjectEditView):
initial_data = {k: request.GET[k] for k in request.GET}
# Set initial site and rack based on side A termination (if not already set)
termination_a_site = getattr(obj.termination_a.parent, 'site', None)
if termination_a_site and 'termination_b_region' not in initial_data:
initial_data['termination_b_region'] = termination_a_site.region
if 'termination_b_site' not in initial_data:
initial_data['termination_b_site'] = getattr(obj.termination_a.parent, 'site', None)
initial_data['termination_b_site'] = termination_a_site
if 'termination_b_rack' not in initial_data:
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent, 'rack', None)

View File

@ -351,10 +351,20 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
label='VRF',
display_field='display_name'
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,
null_option='None'
null_option='None',
query_params={
'region_id': '$region'
}
)
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
@ -363,6 +373,9 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
null_option='None',
query_params={
'site_id': '$site'
},
initial_params={
'vlans': '$vlan'
}
)
vlan = DynamicModelChoiceField(
@ -395,14 +408,6 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
}
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
if instance and instance.vlan is not None:
initial['vlan_group'] = instance.vlan.group
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
@ -472,9 +477,17 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
queryset=Prefix.objects.all(),
widget=forms.MultipleHiddenInput()
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
to_field_name='slug'
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region': '$region'
}
)
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
@ -604,7 +617,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False,
display_field='display_name'
display_field='display_name',
initial_params={
'interfaces': '$interface'
}
)
interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
@ -615,7 +631,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
)
virtual_machine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False
required=False,
initial_params={
'interfaces': '$vminterface'
}
)
vminterface = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
@ -631,10 +650,21 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
label='VRF',
display_field='display_name'
)
nat_region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
label='Region',
initial_params={
'sites': '$nat_site'
}
)
nat_site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,
label='Site'
label='Site',
query_params={
'region_id': '$nat_region'
}
)
nat_rack = DynamicModelChoiceField(
queryset=Rack.objects.all(),
@ -714,10 +744,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
initial = kwargs.get('initial', {}).copy()
if instance:
if type(instance.assigned_object) is Interface:
initial['device'] = instance.assigned_object.device
initial['interface'] = instance.assigned_object
elif type(instance.assigned_object) is VMInterface:
initial['virtual_machine'] = instance.assigned_object.virtual_machine
initial['vminterface'] = instance.assigned_object
if instance.nat_inside:
nat_inside_parent = instance.nat_inside.assigned_object
@ -1028,16 +1056,26 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
#
class VLANGroupForm(BootstrapMixin, forms.ModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region_id': '$region'
}
)
slug = SlugField()
class Meta:
model = VLANGroup
fields = [
'site', 'name', 'slug', 'description',
'region', 'site', 'name', 'slug', 'description',
]
@ -1077,10 +1115,20 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
#
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,
null_option='None'
null_option='None',
query_params={
'region_id': '$region'
}
)
group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
@ -1169,9 +1217,17 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
queryset=VLAN.objects.all(),
widget=forms.MultipleHiddenInput()
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
to_field_name='slug'
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region': '$region'
}
)
group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),

View File

@ -40,6 +40,7 @@
<p class="form-control-static">{{ form.term_side.value }}</p>
</div>
</div>
{% render_field form.region %}
{% render_field form.site %}
</div>
</div>

View File

@ -32,6 +32,12 @@
<div class="panel-body">
{% if termination_a.device %}
{# Device component #}
<div class="form-group">
<label class="col-md-3 control-label required">Region</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.device.site.region }}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Site</label>
<div class="col-md-9">
@ -111,6 +117,9 @@
{% if 'termination_b_provider' in form.fields %}
{% render_field form.termination_b_provider %}
{% endif %}
{% if 'termination_b_region' in form.fields %}
{% render_field form.termination_b_region %}
{% endif %}
{% if 'termination_b_site' in form.fields %}
{% render_field form.termination_b_site %}
{% endif %}

View File

@ -3,10 +3,16 @@
{% block form %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Power Feed</strong></div>
<div class="panel-heading"><strong>Power Panel</strong></div>
<div class="panel-body">
{% render_field form.region %}
{% render_field form.site %}
{% render_field form.power_panel %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Power Feed</strong></div>
<div class="panel-body">
{% render_field form.rack %}
{% render_field form.name %}
{% render_field form.status %}

View File

@ -5,6 +5,7 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>Rack</strong></div>
<div class="panel-body">
{% render_field form.region %}
{% render_field form.site %}
{% render_field form.name %}
{% render_field form.facility_id %}

View File

@ -5,6 +5,7 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>Rack Reservation</strong></div>
<div class="panel-body">
{% render_field form.region %}
{% render_field form.site %}
{% render_field form.rack_group %}
{% render_field form.rack %}

View File

@ -13,6 +13,7 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>Member Devices</strong></div>
<div class="panel-body">
{% render_field form.region %}
{% render_field form.site %}
{% render_field form.rack %}
{% render_field form.members %}

View File

@ -62,6 +62,7 @@
</ul>
<div class="tab-content">
<div class="tab-pane active" id="by_device">
{% render_field form.nat_region %}
{% render_field form.nat_site %}
{% render_field form.nat_rack %}
{% render_field form.nat_device %}

View File

@ -16,6 +16,7 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>Site/VLAN Assignment</strong></div>
<div class="panel-body">
{% render_field form.region %}
{% render_field form.site %}
{% render_field form.vlan_group %}
{% render_field form.vlan %}

View File

@ -8,12 +8,18 @@
{% render_field form.vid %}
{% render_field form.name %}
{% render_field form.status %}
{% render_field form.site %}
{% render_field form.group %}
{% render_field form.role %}
{% render_field form.description %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Assignment</strong></div>
<div class="panel-body">
{% render_field form.region %}
{% render_field form.site %}
{% render_field form.group %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">

View File

@ -8,6 +8,7 @@
{% render_field form.name %}
{% render_field form.type %}
{% render_field form.group %}
{% render_field form.region %}
{% render_field form.site %}
</div>
</div>

View File

@ -119,7 +119,10 @@ class TenancyForm(forms.Form):
tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
null_option='None'
null_option='None',
initial_params={
'tenants': '$tenant'
}
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
@ -129,17 +132,6 @@ class TenancyForm(forms.Form):
}
)
def __init__(self, *args, **kwargs):
# Initialize helper selector
instance = kwargs.get('instance')
if instance and instance.tenant is not None:
initial = kwargs.get('initial', {}).copy()
initial['tenant_group'] = instance.tenant.group
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
class TenancyFilterForm(forms.Form):
tenant_group = DynamicModelMultipleChoiceField(

View File

@ -241,6 +241,7 @@ class DynamicModelChoiceMixin:
"""
:param display_field: The name of the attribute of an API response object to display in the selection list
:param query_params: A dictionary of additional key/value pairs to attach to the API request
:param initial_params: A dictionary of child field references to use for selecting a parent field's initial value
:param null_option: The string used to represent a null selection (if any)
:param disabled_indicator: The name of the field which, if populated, will disable selection of the
choice (optional)
@ -249,10 +250,11 @@ class DynamicModelChoiceMixin:
filter = django_filters.ModelChoiceFilter
widget = widgets.APISelect
def __init__(self, display_field='name', query_params=None, null_option=None, disabled_indicator=None,
brief_mode=True, *args, **kwargs):
def __init__(self, display_field='name', query_params=None, initial_params=None, null_option=None,
disabled_indicator=None, brief_mode=True, *args, **kwargs):
self.display_field = display_field
self.query_params = query_params or {}
self.initial_params = initial_params or {}
self.null_option = null_option
self.disabled_indicator = disabled_indicator
self.brief_mode = brief_mode
@ -293,6 +295,16 @@ class DynamicModelChoiceMixin:
def get_bound_field(self, form, field_name):
bound_field = BoundField(form, self, field_name)
# Set initial value based on prescribed child fields (if not already set)
if not self.initial and self.initial_params:
filter_kwargs = {}
for kwarg, child_field in self.initial_params.items():
value = form.initial.get(child_field.lstrip('$'))
if value:
filter_kwargs[kwarg] = value
if filter_kwargs:
self.initial = self.queryset.filter(**filter_kwargs).first()
# Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
# will be populated on-demand via the APISelect widget.
data = bound_field.value()

View File

@ -7,7 +7,7 @@ from django.http import Http404, HttpResponseRedirect
from django.urls import reverse
from .api import is_api_request
from .views import server_error
from .views import server_error, rest_api_server_error
class LoginRequiredMiddleware(object):
@ -86,6 +86,10 @@ class ExceptionHandlingMiddleware(object):
if isinstance(exception, Http404):
return
# Handle exceptions that occur from REST API requests
if is_api_request(request):
return rest_api_server_error(request)
# Determine the type of exception. If it's a common issue, return a custom error page with instructions.
custom_template = None
if isinstance(exception, ProgrammingError):

View File

@ -13,7 +13,7 @@ from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured, Obje
from django.db import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
from django.http import HttpResponse, HttpResponseServerError
from django.http import HttpResponse, HttpResponseServerError, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader
from django.template.exceptions import TemplateDoesNotExist
@ -27,6 +27,7 @@ from django.views.decorators.csrf import requires_csrf_token
from django.views.defaults import ERROR_500_TEMPLATE_NAME
from django.views.generic import View
from django_tables2 import RequestConfig
from rest_framework import status
from extras.models import CustomField, ExportTemplate
from utilities.exceptions import AbortTransaction
@ -1367,8 +1368,22 @@ def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
type_, error, traceback = sys.exc_info()
return HttpResponseServerError(template.render({
'python_version': platform.python_version(),
'netbox_version': settings.VERSION,
'exception': str(type_),
'error': error,
'exception': str(type_),
'netbox_version': settings.VERSION,
'python_version': platform.python_version(),
}))
def rest_api_server_error(request, *args, **kwargs):
"""
Handle exceptions and return a useful error message for REST API requests.
"""
type_, error, traceback = sys.exc_info()
data = {
'error': str(error),
'exception': type_.__name__,
'netbox_version': settings.VERSION,
'python_version': platform.python_version(),
}
return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -79,9 +79,19 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
queryset=ClusterGroup.objects.all(),
required=False
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region_id': '$region'
}
)
comments = CommentField()
tags = DynamicModelMultipleChoiceField(
@ -92,7 +102,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class Meta:
model = Cluster
fields = (
'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
'name', 'type', 'group', 'tenant', 'region', 'site', 'comments', 'tags',
)
@ -143,9 +153,17 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
queryset=Tenant.objects.all(),
required=False
)
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
to_field_name='slug'
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False
required=False,
query_params={
'region': '$region'
}
)
comments = CommentField(
widget=SmallTextarea,
@ -266,7 +284,10 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
cluster_group = DynamicModelChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
null_option='None'
null_option='None',
initial_params={
'clusters': '$cluster'
}
)
cluster = DynamicModelChoiceField(
queryset=Cluster.objects.all(),
@ -311,14 +332,6 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
}
def __init__(self, *args, **kwargs):
# Initialize helper selector
instance = kwargs.get('instance')
if instance.pk and instance.cluster is not None:
initial = kwargs.get('initial', {}).copy()
initial['cluster_group'] = instance.cluster.group
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if self.instance.pk: