Merge pull request #6230 from netbox-community/develop

Release v2.11.1
This commit is contained in:
Jeremy Stretch 2021-04-21 10:05:35 -04:00 committed by GitHub
commit d8ae65a762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 136 additions and 49 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

@ -1,5 +1,24 @@
# NetBox v2.11 # NetBox v2.11
## v2.11.1 (2021-04-21)
### Enhancements
* [#6161](https://github.com/netbox-community/netbox/issues/6161) - Enable ordering of device component tables
* [#6179](https://github.com/netbox-community/netbox/issues/6179) - Enable natural ordering for virtual machines
* [#6189](https://github.com/netbox-community/netbox/issues/6189) - Add ability to search for locations by name or description
* [#6190](https://github.com/netbox-community/netbox/issues/6190) - Allow filtering devices with no location assigned
* [#6210](https://github.com/netbox-community/netbox/issues/6210) - Include child locations on location view
### Bug Fixes
* [#6184](https://github.com/netbox-community/netbox/issues/6184) - Fix parent object table column in prefix IP addresses list
* [#6188](https://github.com/netbox-community/netbox/issues/6188) - Support custom field filtering for regions, site groups, and locations
* [#6196](https://github.com/netbox-community/netbox/issues/6196) - Fix object list display for users with read-only permissions
* [#6215](https://github.com/netbox-community/netbox/issues/6215) - Restore tenancy section in virtual machine form
---
## v2.11.0 (2021-04-16) ## v2.11.0 (2021-04-16)
**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.

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

@ -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' VERSION = '2.11.1'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

View File

@ -43,13 +43,13 @@
<tr> <tr>
<td>Racks</td> <td>Racks</td>
<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>
<td>Devices</td> <td>Devices</td>
<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>
@ -79,18 +79,18 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Devices</strong> <strong>Locations</strong>
</div> </div>
{% include 'inc/table.html' with table=devices_table %} {% include 'inc/table.html' with table=child_locations_table %}
{% if perms.dcim.add_device %} {% if perms.dcim.add_location %}
<div class="panel-footer text-right noprint"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:device_add' %}?location={{ object.pk }}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}" class="btn btn-xs 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

@ -76,7 +76,9 @@
</div> </div>
</form> </form>
{% else %} {% else %}
{% render_table table 'inc/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

@ -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',)),

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']
] ]

View File

@ -8,7 +8,7 @@ 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