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

Merge branch 'feature' of https://github.com/netbox-community/netbox into feature

# Conflicts:
#	netbox/project-static/js/forms.js
#	netbox/templates/dcim/location.html
#	netbox/templates/generic/object_list.html
This commit is contained in:
checktheroads
2021-04-22 19:19:03 -07:00
24 changed files with 237 additions and 174 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: 🐛 Bug Report name: 🐛 Bug Report
about: Report a reproducible bug in the current release of NetBox description: Report a reproducible bug in the current release of NetBox
labels: ["type: bug"] labels: ["type: bug"]
body: body:
- type: markdown - type: markdown

View File

@ -1,6 +1,6 @@
--- ---
name: 📖 Documentation Change name: 📖 Documentation Change
about: Suggest an addition or modification to the NetBox documentation description: Suggest an addition or modification to the NetBox documentation
labels: ["type: documentation"] labels: ["type: documentation"]
body: body:
- type: dropdown - type: dropdown

View File

@ -1,6 +1,6 @@
--- ---
name: ✨ Feature Request name: ✨ Feature Request
about: Propose a new NetBox feature or enhancement description: Propose a new NetBox feature or enhancement
labels: ["type: feature"] labels: ["type: feature"]
body: body:
- type: markdown - type: markdown

View File

@ -1,6 +1,6 @@
--- ---
name: 🏡 Housekeeping name: 🏡 Housekeeping
about: A change pertaining to the codebase itself (developers only) description: A change pertaining to the codebase itself (developers only)
labels: ["type: housekeeping"] labels: ["type: housekeeping"]
body: body:
- type: markdown - type: markdown

View File

@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.6, 3.7, 3.8] python-version: [3.7, 3.8]
services: services:
redis: redis:
image: redis image: redis

View File

@ -170,14 +170,9 @@ Similar to `ChoiceVar`, but allows for the selection of multiple choices.
A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below. A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below.
* `model` - The model class * `model` - The model class
* `display_field` - The name of the REST API object field to display in the selection list (default: `'display'`)
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional) * `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
* `null_option` - A label representing a "null" or empty choice (optional) * `null_option` - A label representing a "null" or empty choice (optional)
!!! warning
The `display_field` parameter is now deprecated, and will be removed in NetBox v2.12. All ObjectVar instances will
instead use the new standard `display` field for all serializers (introduced in NetBox v2.11).
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status: To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
```python ```python
@ -288,7 +283,6 @@ class NewBranchScript(Script):
switch_model = ObjectVar( switch_model = ObjectVar(
description="Access switch model", description="Access switch model",
model=DeviceType, model=DeviceType,
display_field='model',
query_params={ query_params={
'manufacturer_id': '$manufacturer' 'manufacturer_id': '$manufacturer'
} }

View File

@ -1 +1 @@
version-2.11.md version-2.12.md

View File

@ -1,41 +1,25 @@
# NetBox v2.11 # NetBox v2.11
## v2.11.0 (FUTURE) ## v2.11.1 (2021-04-21)
### Enhancements (from Beta) ### Enhancements
* [#5757](https://github.com/netbox-community/netbox/issues/5757) - Add unique identifier to every object view * [#6161](https://github.com/netbox-community/netbox/issues/6161) - Enable ordering of device component tables
* [#5848](https://github.com/netbox-community/netbox/issues/5848) - Filter custom fields by content type in format `<app_label>.<model>` * [#6179](https://github.com/netbox-community/netbox/issues/6179) - Enable natural ordering for virtual machines
* [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form * [#6189](https://github.com/netbox-community/netbox/issues/6189) - Add ability to search for locations by name or description
* [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views * [#6190](https://github.com/netbox-community/netbox/issues/6190) - Allow filtering devices with no location assigned
* [#6109](https://github.com/netbox-community/netbox/issues/6109) - Add device counts to locations table * [#6210](https://github.com/netbox-community/netbox/issues/6210) - Include child locations on location view
* [#6121](https://github.com/netbox-community/netbox/issues/6121) - Extend parent interface assignment to VM interfaces
* [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page
* [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds
* [#6149](https://github.com/netbox-community/netbox/issues/6149) - Support image attachments for locations
* [#6150](https://github.com/netbox-community/netbox/issues/6150) - Enable change logging for journal entries
### Bug Fixes (from Beta) ### Bug Fixes
* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags * [#6184](https://github.com/netbox-community/netbox/issues/6184) - Fix parent object table column in prefix IP addresses list
* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link * [#6188](https://github.com/netbox-community/netbox/issues/6188) - Support custom field filtering for regions, site groups, and locations
* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table * [#6196](https://github.com/netbox-community/netbox/issues/6196) - Fix object list display for users with read-only permissions
* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view * [#6215](https://github.com/netbox-community/netbox/issues/6215) - Restore tenancy section in virtual machine form
* [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface
* [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form
* [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export
* [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location
* [#6124](https://github.com/netbox-community/netbox/issues/6124) - Location `parent` filter should return all child locations (not just those directly assigned)
* [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list
* [#6155](https://github.com/netbox-community/netbox/issues/6155) - Fix admin links for plugins, background tasks
* [#6171](https://github.com/netbox-community/netbox/issues/6171) - Fix display of horizontally-scrolling object lists
* [#6173](https://github.com/netbox-community/netbox/issues/6173) - Fix assigned device/VM count when bulk editing/deleting device roles
--- ---
## v2.11-beta1 (2021-04-06) ## v2.11.0 (2021-04-16)
**WARNING:** This is a beta release and is not suitable for production use. It is intended for development and evaluation purposes only. No upgrade path to the final v2.11 release will be provided from this beta, and users should assume that all data entered into the application will be lost.
**Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v2.12, Python 3.7 or later will be required. **Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v2.12, Python 3.7 or later will be required.
@ -126,7 +110,9 @@ A new provider network model has been introduced to represent the boundary of a
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields * [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links * [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks * [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
* [#5757](https://github.com/netbox-community/netbox/issues/5757) - Add unique identifier to every object view
* [#5830](https://github.com/netbox-community/netbox/issues/5830) - Add `as_attachment` to ExportTemplate to control download behavior * [#5830](https://github.com/netbox-community/netbox/issues/5830) - Add `as_attachment` to ExportTemplate to control download behavior
* [#5848](https://github.com/netbox-community/netbox/issues/5848) - Filter custom fields by content type in format `<app_label>.<model>`
* [#5891](https://github.com/netbox-community/netbox/issues/5891) - Add `display` field to all REST API serializers * [#5891](https://github.com/netbox-community/netbox/issues/5891) - Add `display` field to all REST API serializers
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI * [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location * [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
@ -136,6 +122,29 @@ A new provider network model has been introduced to represent the boundary of a
* [#5975](https://github.com/netbox-community/netbox/issues/5975) - Allow partial (decimal) vCPU allocations for virtual machines * [#5975](https://github.com/netbox-community/netbox/issues/5975) - Allow partial (decimal) vCPU allocations for virtual machines
* [#6001](https://github.com/netbox-community/netbox/issues/6001) - Paginate component tables under device views * [#6001](https://github.com/netbox-community/netbox/issues/6001) - Paginate component tables under device views
* [#6038](https://github.com/netbox-community/netbox/issues/6038) - Include tagged objects list on tag view * [#6038](https://github.com/netbox-community/netbox/issues/6038) - Include tagged objects list on tag view
* [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form
* [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views
* [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page
* [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds
* [#6149](https://github.com/netbox-community/netbox/issues/6149) - Support image attachments for locations
### Bug Fixes (from v2.11-beta1)
* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags
* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link
* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table
* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view
* [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface
* [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form
* [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export
* [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location
* [#6124](https://github.com/netbox-community/netbox/issues/6124) - Location `parent` filter should return all child locations (not just those directly assigned)
* [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list
* [#6155](https://github.com/netbox-community/netbox/issues/6155) - Fix admin links for plugins, background tasks
* [#6171](https://github.com/netbox-community/netbox/issues/6171) - Fix display of horizontally-scrolling object lists
* [#6173](https://github.com/netbox-community/netbox/issues/6173) - Fix assigned device/VM count when bulk editing/deleting device roles
* [#6176](https://github.com/netbox-community/netbox/issues/6176) - Correct position of MAC address field when creating VM interfaces
* [#6177](https://github.com/netbox-community/netbox/issues/6177) - Prevent VM interface from being assigned as its own parent
### Other Changes ### Other Changes

View File

@ -0,0 +1,8 @@
# NetBox v2.12
## v2.12-beta1 (FUTURE)
### Other Changes
* [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6
* [#5994](https://github.com/netbox-community/netbox/issues/5994) - Drop support for `display_field` argument on ObjectVar

View File

@ -209,6 +209,14 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
model = Location model = Location
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):

View File

@ -230,7 +230,7 @@ class RegionBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
nullable_fields = ['parent', 'description'] nullable_fields = ['parent', 'description']
class RegionFilterForm(BootstrapMixin, forms.Form): class RegionFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site model = Site
q = forms.CharField( q = forms.CharField(
required=False, required=False,
@ -287,8 +287,8 @@ class SiteGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
nullable_fields = ['parent', 'description'] nullable_fields = ['parent', 'description']
class SiteGroupFilterForm(BootstrapMixin, forms.Form): class SiteGroupFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Site model = SiteGroup
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label=_('Search') label=_('Search')
@ -557,7 +557,12 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
nullable_fields = ['parent', 'description'] nullable_fields = ['parent', 'description']
class LocationFilterForm(BootstrapMixin, forms.Form): class LocationFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Location
q = forms.CharField(
required=False,
label=_('Search')
)
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
@ -2424,10 +2429,11 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
location_id = DynamicModelMultipleChoiceField( location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
label=_('Location'), null_option='None',
query_params={ query_params={
'site_id': '$site_id' 'site_id': '$site_id'
} },
label=_('Location')
) )
rack_id = DynamicModelMultipleChoiceField( rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),

View File

@ -291,6 +291,7 @@ class ConsolePortTable(DeviceComponentTable, PathEndpointTable):
class DeviceConsolePortTable(ConsolePortTable): class DeviceConsolePortTable(ConsolePortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@ -335,6 +336,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-console-network-outline"></i> ' template_code='<i class="mdi mdi-console-network-outline"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>', '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@ -379,6 +381,7 @@ class DevicePowerPortTable(PowerPortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">' template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
'{{ value }}</a>', '{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@ -428,6 +431,7 @@ class PowerOutletTable(DeviceComponentTable, PathEndpointTable):
class DevicePowerOutletTable(PowerOutletTable): class DevicePowerOutletTable(PowerOutletTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@ -492,6 +496,7 @@ class DeviceInterfaceTable(InterfaceTable):
template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant' template_code='<i class="mdi mdi-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}drag-horizontal-variant'
'{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet' '{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}ethernet'
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', '{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
parent = tables.Column( parent = tables.Column(
@ -555,6 +560,7 @@ class DeviceFrontPortTable(FrontPortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> ' template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>', '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@ -602,6 +608,7 @@ class DeviceRearPortTable(RearPortTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> ' template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>', '<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@ -651,6 +658,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}' template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
'"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>', '"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(
@ -698,6 +706,7 @@ class DeviceInventoryItemTable(InventoryItemTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">' template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
'{{ value }}</a>', '{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}} attrs={'td': {'class': 'text-nowrap'}}
) )
actions = ButtonsColumn( actions = ButtonsColumn(

View File

@ -364,16 +364,30 @@ class LocationView(generic.ObjectView):
queryset = Location.objects.all() queryset = Location.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
devices = Device.objects.restrict(request.user, 'view').filter( location_ids = instance.get_descendants(include_self=True).values_list('pk', flat=True)
location=instance rack_count = Rack.objects.filter(location__in=location_ids).count()
) device_count = Device.objects.filter(location__in=location_ids).count()
devices_table = tables.DeviceTable(devices) child_locations = Location.objects.add_related_count(
devices_table.columns.hide('location') Location.objects.add_related_count(
paginate_table(devices_table, request) Location.objects.all(),
Device,
'location',
'device_count',
cumulative=True
),
Rack,
'location',
'rack_count',
cumulative=True
).filter(pk__in=location_ids).exclude(pk=instance.pk)
child_locations_table = tables.LocationTable(child_locations)
paginate_table(child_locations_table, request)
return { return {
'devices_table': devices_table, 'rack_count': rack_count,
'device_count': device_count,
'child_locations_table': child_locations_table,
} }
@ -1305,8 +1319,7 @@ class DeviceConsolePortsView(generic.ObjectView):
) )
consoleport_table = tables.DeviceConsolePortTable( consoleport_table = tables.DeviceConsolePortTable(
data=consoleports, data=consoleports,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'): if request.user.has_perm('dcim.change_consoleport') or request.user.has_perm('dcim.delete_consoleport'):
consoleport_table.columns.show('pk') consoleport_table.columns.show('pk')
@ -1330,8 +1343,7 @@ class DeviceConsoleServerPortsView(generic.ObjectView):
) )
consoleserverport_table = tables.DeviceConsoleServerPortTable( consoleserverport_table = tables.DeviceConsoleServerPortTable(
data=consoleserverports, data=consoleserverports,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_consoleserverport') or \ if request.user.has_perm('dcim.change_consoleserverport') or \
request.user.has_perm('dcim.delete_consoleserverport'): request.user.has_perm('dcim.delete_consoleserverport'):
@ -1354,8 +1366,7 @@ class DevicePowerPortsView(generic.ObjectView):
) )
powerport_table = tables.DevicePowerPortTable( powerport_table = tables.DevicePowerPortTable(
data=powerports, data=powerports,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'): if request.user.has_perm('dcim.change_powerport') or request.user.has_perm('dcim.delete_powerport'):
powerport_table.columns.show('pk') powerport_table.columns.show('pk')
@ -1377,8 +1388,7 @@ class DevicePowerOutletsView(generic.ObjectView):
) )
poweroutlet_table = tables.DevicePowerOutletTable( poweroutlet_table = tables.DevicePowerOutletTable(
data=poweroutlets, data=poweroutlets,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'): if request.user.has_perm('dcim.change_poweroutlet') or request.user.has_perm('dcim.delete_poweroutlet'):
poweroutlet_table.columns.show('pk') poweroutlet_table.columns.show('pk')
@ -1402,8 +1412,7 @@ class DeviceInterfacesView(generic.ObjectView):
) )
interface_table = tables.DeviceInterfaceTable( interface_table = tables.DeviceInterfaceTable(
data=interfaces, data=interfaces,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'): if request.user.has_perm('dcim.change_interface') or request.user.has_perm('dcim.delete_interface'):
interface_table.columns.show('pk') interface_table.columns.show('pk')
@ -1425,8 +1434,7 @@ class DeviceFrontPortsView(generic.ObjectView):
) )
frontport_table = tables.DeviceFrontPortTable( frontport_table = tables.DeviceFrontPortTable(
data=frontports, data=frontports,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'): if request.user.has_perm('dcim.change_frontport') or request.user.has_perm('dcim.delete_frontport'):
frontport_table.columns.show('pk') frontport_table.columns.show('pk')
@ -1446,8 +1454,7 @@ class DeviceRearPortsView(generic.ObjectView):
rearports = RearPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related('cable') rearports = RearPort.objects.restrict(request.user, 'view').filter(device=instance).prefetch_related('cable')
rearport_table = tables.DeviceRearPortTable( rearport_table = tables.DeviceRearPortTable(
data=rearports, data=rearports,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'): if request.user.has_perm('dcim.change_rearport') or request.user.has_perm('dcim.delete_rearport'):
rearport_table.columns.show('pk') rearport_table.columns.show('pk')
@ -1469,8 +1476,7 @@ class DeviceDeviceBaysView(generic.ObjectView):
) )
devicebay_table = tables.DeviceDeviceBayTable( devicebay_table = tables.DeviceDeviceBayTable(
data=devicebays, data=devicebays,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'): if request.user.has_perm('dcim.change_devicebay') or request.user.has_perm('dcim.delete_devicebay'):
devicebay_table.columns.show('pk') devicebay_table.columns.show('pk')
@ -1492,8 +1498,7 @@ class DeviceInventoryView(generic.ObjectView):
).prefetch_related('manufacturer') ).prefetch_related('manufacturer')
inventoryitem_table = tables.DeviceInventoryItemTable( inventoryitem_table = tables.DeviceInventoryItemTable(
data=inventoryitems, data=inventoryitems,
user=request.user, user=request.user
orderable=False
) )
if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'): if request.user.has_perm('dcim.change_inventoryitem') or request.user.has_perm('dcim.delete_inventoryitem'):
inventoryitem_table.columns.show('pk') inventoryitem_table.columns.show('pk')

View File

@ -180,27 +180,16 @@ class ObjectVar(ScriptVariable):
A single object within NetBox. A single object within NetBox.
:param model: The NetBox model being referenced :param model: The NetBox model being referenced
:param display_field: The attribute of the returned object to display in the selection list (DEPRECATED)
:param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional) :param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional)
:param null_option: The label to use as a "null" selection option (optional) :param null_option: The label to use as a "null" selection option (optional)
""" """
form_field = DynamicModelChoiceField form_field = DynamicModelChoiceField
def __init__(self, model, query_params=None, null_option=None, *args, **kwargs): def __init__(self, model, query_params=None, null_option=None, *args, **kwargs):
# TODO: Remove display_field in v2.12
if 'display_field' in kwargs:
warnings.warn(
"The 'display_field' parameter has been deprecated, and will be removed in NetBox v2.12. Object "
"variables will now reference the 'display' attribute available on all model serializers by default."
)
display_field = kwargs.pop('display_field', 'display')
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.field_attrs.update({ self.field_attrs.update({
'queryset': model.objects.all(), 'queryset': model.objects.all(),
'display_field': display_field,
'query_params': query_params, 'query_params': query_params,
'null_option': null_option, 'null_option': null_option,
}) })

View File

@ -340,10 +340,10 @@ class IPAddressTable(BaseTable):
verbose_name='Interface' verbose_name='Interface'
) )
assigned_object_parent = tables.Column( assigned_object_parent = tables.Column(
accessor='assigned_object__parent', accessor='assigned_object.parent_object',
linkify=True, linkify=True,
orderable=False, orderable=False,
verbose_name='Interface Parent' verbose_name='Device/VM'
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):

View File

@ -16,7 +16,7 @@ from django.core.validators import URLValidator
# Environment setup # Environment setup
# #
VERSION = '2.11.0-dev' VERSION = '2.12-beta1'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()
@ -25,15 +25,9 @@ HOSTNAME = platform.node()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Validate Python version # Validate Python version
if platform.python_version_tuple() < ('3', '6'):
raise RuntimeError(
"NetBox requires Python 3.6 or higher (current: Python {})".format(platform.python_version())
)
# TODO: Remove in NetBox v2.12
if platform.python_version_tuple() < ('3', '7'): if platform.python_version_tuple() < ('3', '7'):
warnings.warn( raise RuntimeError(
"Support for Python 3.6 will be dropped in NetBox v2.12. Please upgrade to Python 3.7 or later at your " f"NetBox requires Python 3.7 or higher (current: Python {platform.python_version()})"
"earliest convenience."
) )

View File

@ -44,13 +44,13 @@
<tr> <tr>
<th scope="row">Racks</th> <th scope="row">Racks</th>
<td> <td>
<a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ object.racks.count }}</a> <a href="{% url 'dcim:rack_list' %}?location_id={{ object.pk }}">{{ rack_count }}</a>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Devices</th> <th scope="row">Devices</th>
<td> <td>
<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}">{{ devices_table.rows|length }}</a> <a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}">{{ device_count }}</a>
</td> </td>
</tr> </tr>
</table> </table>
@ -83,20 +83,20 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Devices Locations
</h5> </h5>
<div class="card-body"> <div class="card-body">
{% include 'inc/table.html' with table=devices_table %} {% include 'inc/table.html' with table=child_locations_table %}
</div> </div>
{% if perms.dcim.add_device %} {% if perms.dcim.add_location %}
<div class="card-footer text-end noprint"> <div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_add' %}?location={{ object.pk }}" class="btn btn-sm btn-primary"> <a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Device <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Location
</a> </a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %} {% include 'inc/paginator.html' with paginator=child_locations_table.paginator page=child_locations_table.page %}
{% plugin_full_width_page object %} {% plugin_full_width_page object %}
</div> </div>
</div> </div>

View File

@ -92,7 +92,9 @@
</div> </div>
</form> </form>
{% else %} {% else %}
{% include table_template|default:'responsive_table.html' %} <div class="table-responsive">
{% render_table table 'inc/table.html' %}
</div>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}

View File

@ -328,7 +328,6 @@ class ExpandableIPAddressField(forms.CharField):
class DynamicModelChoiceMixin: 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 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 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)
@ -338,10 +337,8 @@ class DynamicModelChoiceMixin:
filter = django_filters.ModelChoiceFilter filter = django_filters.ModelChoiceFilter
widget = widgets.APISelect widget = widgets.APISelect
# TODO: Remove display_field in v2.12 def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, *args,
def __init__(self, display_field='display', query_params=None, initial_params=None, null_option=None, **kwargs):
disabled_indicator=None, *args, **kwargs):
self.display_field = display_field
self.query_params = query_params or {} self.query_params = query_params or {}
self.initial_params = initial_params or {} self.initial_params = initial_params or {}
self.null_option = null_option self.null_option = null_option
@ -354,9 +351,7 @@ class DynamicModelChoiceMixin:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def widget_attrs(self, widget): def widget_attrs(self, widget):
attrs = { attrs = {}
'display-field': self.display_field,
}
# Set value-field attribute if the field specifies to_field_name # Set value-field attribute if the field specifies to_field_name
if self.to_field_name: if self.to_field_name:

View File

@ -376,6 +376,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
fieldsets = ( fieldsets = (
('Virtual Machine', ('name', 'role', 'status', 'tags')), ('Virtual Machine', ('name', 'role', 'status', 'tags')),
('Cluster', ('cluster_group', 'cluster')), ('Cluster', ('cluster_group', 'cluster')),
('Tenancy', ('tenant_group', 'tenant')),
('Management', ('platform', 'primary_ip4', 'primary_ip6')), ('Management', ('platform', 'primary_ip4', 'primary_ip6')),
('Resources', ('vcpus', 'memory', 'disk')), ('Resources', ('vcpus', 'memory', 'disk')),
('Config Context', ('local_context_data',)), ('Config Context', ('local_context_data',)),
@ -666,7 +667,6 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=VMInterface.objects.all(), queryset=VMInterface.objects.all(),
required=False, required=False,
display_field='display_name',
query_params={ query_params={
'virtualmachine_id': 'virtual_machine', 'virtualmachine_id': 'virtual_machine',
} }
@ -702,6 +702,10 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
required=False required=False
) )
field_order = (
'virtual_machine', 'name_pattern', 'enabled', 'parent', 'mtu', 'mac_address', 'description', 'mode',
'untagged_vlan', 'tagged_vlans', 'tags'
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -751,8 +755,7 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
) )
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
queryset=VMInterface.objects.all(), queryset=VMInterface.objects.all(),
required=False, required=False
display_field='display_name'
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
required=False, required=False,

View File

@ -0,0 +1,32 @@
from django.db import migrations
import utilities.fields
import utilities.ordering
def naturalize_virtualmachines(apps, schema_editor):
VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
for name in VirtualMachine.objects.values_list('name', flat=True).order_by('name').distinct():
VirtualMachine.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0022_vminterface_parent'),
]
operations = [
migrations.AlterModelOptions(
name='virtualmachine',
options={'ordering': ('_name', 'pk')},
),
migrations.AddField(
model_name='virtualmachine',
name='_name',
field=utilities.fields.NaturalOrderingField('name', max_length=100, blank=True, naturalize_function=utilities.ordering.naturalize),
),
migrations.RunPython(
code=naturalize_virtualmachines,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -226,6 +226,11 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
status = models.CharField( status = models.CharField(
max_length=50, max_length=50,
choices=VirtualMachineStatusChoices, choices=VirtualMachineStatusChoices,
@ -296,7 +301,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
] ]
class Meta: class Meta:
ordering = ('name', 'pk') # Name may be non-unique ordering = ('_name', 'pk') # Name may be non-unique
unique_together = [ unique_together = [
['cluster', 'tenant', 'name'] ['cluster', 'tenant', 'name']
] ]
@ -463,6 +468,10 @@ class VMInterface(PrimaryModel, BaseInterface):
f"({self.parent.virtual_machine})." f"({self.parent.virtual_machine})."
}) })
# An interface cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({'parent': "An interface cannot be its own parent."})
# Validate untagged VLAN # Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]: if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
raise ValidationError({ raise ValidationError({

View File

@ -1,14 +1,14 @@
Django==3.2 Django==3.2
django-cacheops==5.1 django-cacheops==5.1
django-cors-headers==3.7.0 django-cors-headers==3.7.0
django-debug-toolbar==3.2 django-debug-toolbar==3.2.1
django-filter==2.4.0 django-filter==2.4.0
django-mptt==0.12.0 django-mptt==0.12.0
django-pglocks==1.0.4 django-pglocks==1.0.4
django-prometheus==2.1.0 django-prometheus==2.1.0
django-rq==2.4.1 django-rq==2.4.1
django-tables2==2.3.4 django-tables2==2.3.4
django-taggit==1.3.0 django-taggit==1.4.0
django-timezone-field==4.1.2 django-timezone-field==4.1.2
djangorestframework==3.12.4 djangorestframework==3.12.4
drf-yasg[validation]==1.20.0 drf-yasg[validation]==1.20.0