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 ## 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 ```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 # 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) ## v2.9.8 (2020-10-30)
### Enhancements ### Enhancements

View File

@ -303,14 +303,24 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
# #
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all() queryset=Site.objects.all(),
query_params={
'region_id': '$region'
}
) )
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ 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 = { help_texts = {
'port_speed': "Physical circuit speed", 'port_speed': "Physical circuit speed",

View File

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

View File

@ -100,23 +100,6 @@ class DeviceTestCase(TestCase):
self.assertIn('face', form.errors) self.assertIn('face', form.errors)
self.assertIn('position', 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): class LabelTestCase(TestCase):

View File

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

View File

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

View File

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

View File

@ -3,10 +3,16 @@
{% block form %} {% block form %}
<div class="panel panel-default"> <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"> <div class="panel-body">
{% render_field form.region %}
{% render_field form.site %} {% render_field form.site %}
{% render_field form.power_panel %} {% 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.rack %}
{% render_field form.name %} {% render_field form.name %}
{% render_field form.status %} {% render_field form.status %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -119,7 +119,10 @@ class TenancyForm(forms.Form):
tenant_group = DynamicModelChoiceField( tenant_group = DynamicModelChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
required=False, required=False,
null_option='None' null_option='None',
initial_params={
'tenants': '$tenant'
}
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), 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): class TenancyFilterForm(forms.Form):
tenant_group = DynamicModelMultipleChoiceField( 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 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 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 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 :param disabled_indicator: The name of the field which, if populated, will disable selection of the
choice (optional) choice (optional)
@ -249,10 +250,11 @@ class DynamicModelChoiceMixin:
filter = django_filters.ModelChoiceFilter filter = django_filters.ModelChoiceFilter
widget = widgets.APISelect widget = widgets.APISelect
def __init__(self, display_field='name', query_params=None, null_option=None, disabled_indicator=None, def __init__(self, display_field='name', query_params=None, initial_params=None, null_option=None,
brief_mode=True, *args, **kwargs): disabled_indicator=None, brief_mode=True, *args, **kwargs):
self.display_field = display_field self.display_field = display_field
self.query_params = query_params or {} self.query_params = query_params or {}
self.initial_params = initial_params or {}
self.null_option = null_option self.null_option = null_option
self.disabled_indicator = disabled_indicator self.disabled_indicator = disabled_indicator
self.brief_mode = brief_mode self.brief_mode = brief_mode
@ -293,6 +295,16 @@ class DynamicModelChoiceMixin:
def get_bound_field(self, form, field_name): def get_bound_field(self, form, field_name):
bound_field = BoundField(form, self, 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 # 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. # will be populated on-demand via the APISelect widget.
data = bound_field.value() data = bound_field.value()

View File

@ -7,7 +7,7 @@ from django.http import Http404, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from .api import is_api_request from .api import is_api_request
from .views import server_error from .views import server_error, rest_api_server_error
class LoginRequiredMiddleware(object): class LoginRequiredMiddleware(object):
@ -86,6 +86,10 @@ class ExceptionHandlingMiddleware(object):
if isinstance(exception, Http404): if isinstance(exception, Http404):
return 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. # Determine the type of exception. If it's a common issue, return a custom error page with instructions.
custom_template = None custom_template = None
if isinstance(exception, ProgrammingError): 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 import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError from django.db.models import ManyToManyField, ProtectedError
from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea 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.shortcuts import get_object_or_404, redirect, render
from django.template import loader from django.template import loader
from django.template.exceptions import TemplateDoesNotExist 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.defaults import ERROR_500_TEMPLATE_NAME
from django.views.generic import View from django.views.generic import View
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
from rest_framework import status
from extras.models import CustomField, ExportTemplate from extras.models import CustomField, ExportTemplate
from utilities.exceptions import AbortTransaction 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() type_, error, traceback = sys.exc_info()
return HttpResponseServerError(template.render({ return HttpResponseServerError(template.render({
'python_version': platform.python_version(),
'netbox_version': settings.VERSION,
'exception': str(type_),
'error': error, '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(), queryset=ClusterGroup.objects.all(),
required=False required=False
) )
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False,
query_params={
'region_id': '$region'
}
) )
comments = CommentField() comments = CommentField()
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
@ -92,7 +102,7 @@ class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class Meta: class Meta:
model = Cluster model = Cluster
fields = ( 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(), queryset=Tenant.objects.all(),
required=False required=False
) )
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
required=False,
to_field_name='slug'
)
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False,
query_params={
'region': '$region'
}
) )
comments = CommentField( comments = CommentField(
widget=SmallTextarea, widget=SmallTextarea,
@ -266,7 +284,10 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
cluster_group = DynamicModelChoiceField( cluster_group = DynamicModelChoiceField(
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
required=False, required=False,
null_option='None' null_option='None',
initial_params={
'clusters': '$cluster'
}
) )
cluster = DynamicModelChoiceField( cluster = DynamicModelChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
@ -311,14 +332,6 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
} }
def __init__(self, *args, **kwargs): 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) super().__init__(*args, **kwargs)
if self.instance.pk: if self.instance.pk: