mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 12:42:52 -06:00
Merge branch 'feature' into 15278-primary-nested-serializers
This commit is contained in:
commit
cd74e040c1
@ -476,7 +476,7 @@ class NewBranchScript(Script):
|
|||||||
name=f'{site.slug}-switch{i}',
|
name=f'{site.slug}-switch{i}',
|
||||||
site=site,
|
site=site,
|
||||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||||
device_role=switch_role
|
role=switch_role
|
||||||
)
|
)
|
||||||
switch.full_clean()
|
switch.full_clean()
|
||||||
switch.save()
|
switch.save()
|
||||||
|
@ -18,9 +18,9 @@ When a device has one or more interfaces with IP addresses assigned, a primary I
|
|||||||
|
|
||||||
The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant.
|
The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant.
|
||||||
|
|
||||||
### Device Role
|
### Role
|
||||||
|
|
||||||
The functional [role](./devicerole.md) assigned to this device.
|
The functional [device role](./devicerole.md) assigned to this device.
|
||||||
|
|
||||||
### Device Type
|
### Device Type
|
||||||
|
|
||||||
|
@ -13,6 +13,10 @@
|
|||||||
|
|
||||||
The NetBox user interface has been completely refreshed and updated.
|
The NetBox user interface has been completely refreshed and updated.
|
||||||
|
|
||||||
|
#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087))
|
||||||
|
|
||||||
|
The REST API now supports specifying which fields to include in the response data.
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
|
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
|
||||||
@ -22,6 +26,9 @@ The NetBox user interface has been completely refreshed and updated.
|
|||||||
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
|
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
|
||||||
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
|
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
|
||||||
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
|
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
|
||||||
|
* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects
|
||||||
|
* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets
|
||||||
|
* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
@ -34,5 +41,6 @@ The NetBox user interface has been completely refreshed and updated.
|
|||||||
* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin`
|
* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin`
|
||||||
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`)
|
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`)
|
||||||
* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class
|
* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class
|
||||||
|
* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features
|
||||||
* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
|
* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
|
||||||
* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
|
* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
|
||||||
|
@ -32,11 +32,6 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
device_type = DeviceTypeSerializer(nested=True)
|
device_type = DeviceTypeSerializer(nested=True)
|
||||||
role = DeviceRoleSerializer(nested=True)
|
role = DeviceRoleSerializer(nested=True)
|
||||||
device_role = DeviceRoleSerializer(
|
|
||||||
nested=True,
|
|
||||||
read_only=True,
|
|
||||||
help_text='Deprecated in v3.6 in favor of `role`.'
|
|
||||||
)
|
|
||||||
tenant = TenantSerializer(
|
tenant = TenantSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
required=False,
|
required=False,
|
||||||
@ -83,13 +78,13 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
|
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
|
||||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||||
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||||
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags',
|
'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields',
|
||||||
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
|
'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count',
|
||||||
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
|
'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count',
|
||||||
'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
'module_bay_count', 'inventory_item_count',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||||
|
|
||||||
@ -104,22 +99,19 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||||||
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_device_role(self, obj):
|
|
||||||
return obj.role
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||||
config_context = serializers.SerializerMethodField(read_only=True)
|
config_context = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta(DeviceSerializer.Meta):
|
class Meta(DeviceSerializer.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
|
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
|
||||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||||
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||||
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
|
'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags',
|
||||||
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
|
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
|
||||||
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
|
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
|
||||||
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
@ -815,20 +815,6 @@ class Device(
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:device', args=[self.pk])
|
return reverse('dcim:device', args=[self.pk])
|
||||||
|
|
||||||
@property
|
|
||||||
def device_role(self):
|
|
||||||
"""
|
|
||||||
For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device.
|
|
||||||
"""
|
|
||||||
return self.role
|
|
||||||
|
|
||||||
@device_role.setter
|
|
||||||
def device_role(self, value):
|
|
||||||
"""
|
|
||||||
For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device.
|
|
||||||
"""
|
|
||||||
self.role = value
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase):
|
|||||||
device = Device.objects.create(
|
device = Device.objects.create(
|
||||||
site=self.site,
|
site=self.site,
|
||||||
device_type=self.device.device_type,
|
device_type=self.device.device_type,
|
||||||
device_role=self.device.device_role,
|
role=self.device.role,
|
||||||
name='Test mid-span Device'
|
name='Test mid-span Device'
|
||||||
)
|
)
|
||||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||||
|
@ -533,30 +533,6 @@ class DeviceTestCase(TestCase):
|
|||||||
device2.full_clean()
|
device2.full_clean()
|
||||||
device2.save()
|
device2.save()
|
||||||
|
|
||||||
def test_old_device_role_field(self):
|
|
||||||
"""
|
|
||||||
Ensure that the old device role field sets the value in the new role field.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Test getter method
|
|
||||||
device = Device(
|
|
||||||
site=Site.objects.first(),
|
|
||||||
device_type=DeviceType.objects.first(),
|
|
||||||
role=DeviceRole.objects.first(),
|
|
||||||
name='Test Device 1',
|
|
||||||
device_role=DeviceRole.objects.first()
|
|
||||||
)
|
|
||||||
device.full_clean()
|
|
||||||
device.save()
|
|
||||||
|
|
||||||
self.assertEqual(device.role, device.device_role)
|
|
||||||
|
|
||||||
# Test setter method
|
|
||||||
device.device_role = DeviceRole.objects.last()
|
|
||||||
device.full_clean()
|
|
||||||
device.save()
|
|
||||||
self.assertEqual(device.role, device.device_role)
|
|
||||||
|
|
||||||
|
|
||||||
class CableTestCase(TestCase):
|
class CableTestCase(TestCase):
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@ from collections import defaultdict
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend
|
from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend
|
||||||
from django.contrib.auth.models import Group, AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from users.constants import CONSTRAINT_TOKEN_USER
|
from users.constants import CONSTRAINT_TOKEN_USER
|
||||||
from users.models import ObjectPermission
|
from users.models import Group, ObjectPermission
|
||||||
from utilities.permissions import (
|
from utilities.permissions import (
|
||||||
permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct,
|
permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct,
|
||||||
)
|
)
|
||||||
|
@ -392,19 +392,19 @@ ADMIN_MENU = Menu(
|
|||||||
),
|
),
|
||||||
# Proxy model for auth.Group
|
# Proxy model for auth.Group
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link=f'users:netboxgroup_list',
|
link=f'users:group_list',
|
||||||
link_text=_('Groups'),
|
link_text=_('Groups'),
|
||||||
permissions=[f'auth.view_group'],
|
permissions=[f'auth.view_group'],
|
||||||
staff_only=True,
|
staff_only=True,
|
||||||
buttons=(
|
buttons=(
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
link=f'users:netboxgroup_add',
|
link=f'users:group_add',
|
||||||
title='Add',
|
title='Add',
|
||||||
icon_class='mdi mdi-plus-thick',
|
icon_class='mdi mdi-plus-thick',
|
||||||
permissions=[f'auth.add_group']
|
permissions=[f'auth.add_group']
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
link=f'users:netboxgroup_import',
|
link=f'users:group_import',
|
||||||
title='Import',
|
title='Import',
|
||||||
icon_class='mdi mdi-upload',
|
icon_class='mdi mdi-upload',
|
||||||
permissions=[f'auth.add_group']
|
permissions=[f'auth.add_group']
|
||||||
|
@ -2,7 +2,6 @@ import datetime
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
@ -12,7 +11,7 @@ from rest_framework.test import APIClient
|
|||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from ipam.models import Prefix
|
from ipam.models import Prefix
|
||||||
from users.models import ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
from utilities.testing import TestCase
|
from utilities.testing import TestCase
|
||||||
from utilities.testing.api import APITestCase
|
from utilities.testing.api import APITestCase
|
||||||
|
|
||||||
|
BIN
netbox/project-static/dist/netbox.css
vendored
BIN
netbox/project-static/dist/netbox.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 109 B |
@ -18,11 +18,12 @@ function handleSelection(link: HTMLAnchorElement): void {
|
|||||||
const value = link.getAttribute('data-value');
|
const value = link.getAttribute('data-value');
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
target.slim.setData([
|
target.tomselect.addOption({
|
||||||
{text: label, value: value}
|
id: value,
|
||||||
]);
|
display: label,
|
||||||
const change = new Event('change');
|
});
|
||||||
target.dispatchEvent(change);
|
//@ts-ignore
|
||||||
|
target.tomselect.addItem(value);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ function renderItem(data: TomOption, escape: typeof escape_html) {
|
|||||||
|
|
||||||
// Initialize <select> elements which are populated via a REST API call
|
// Initialize <select> elements which are populated via a REST API call
|
||||||
export function initDynamicSelects(): void {
|
export function initDynamicSelects(): void {
|
||||||
for (const select of getElements<HTMLSelectElement>('select.api-select')) {
|
for (const select of getElements<HTMLSelectElement>('select.api-select:not(.tomselected)')) {
|
||||||
new DynamicTomSelect(select, {
|
new DynamicTomSelect(select, {
|
||||||
...config,
|
...config,
|
||||||
valueField: VALUE_FIELD,
|
valueField: VALUE_FIELD,
|
||||||
|
@ -7,7 +7,7 @@ import { getElements } from '../util';
|
|||||||
// Initialize <select> elements with statically-defined options
|
// Initialize <select> elements with statically-defined options
|
||||||
export function initStaticSelects(): void {
|
export function initStaticSelects(): void {
|
||||||
for (const select of getElements<HTMLSelectElement>(
|
for (const select of getElements<HTMLSelectElement>(
|
||||||
'select:not(.api-select):not(.color-select)',
|
'select:not(.tomselected):not(.no-ts):not([size]):not(.api-select):not(.color-select)',
|
||||||
)) {
|
)) {
|
||||||
new TomSelect(select, {
|
new TomSelect(select, {
|
||||||
...config,
|
...config,
|
||||||
@ -24,7 +24,7 @@ export function initColorSelects(): void {
|
|||||||
)}"></span> ${escape(item.text)}</div>`;
|
)}"></span> ${escape(item.text)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const select of getElements<HTMLSelectElement>('select.color-select')) {
|
for (const select of getElements<HTMLSelectElement>('select.color-select:not(.tomselected)')) {
|
||||||
new TomSelect(select, {
|
new TomSelect(select, {
|
||||||
...config,
|
...config,
|
||||||
maxOptions: undefined,
|
maxOptions: undefined,
|
||||||
|
@ -244,29 +244,6 @@ export function getSelectedOptions<E extends HTMLElement>(
|
|||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get data that can only be accessed via Django context, and is thus already rendered in the HTML
|
|
||||||
* template.
|
|
||||||
*
|
|
||||||
* @see Templates requiring Django context data have a `{% block data %}` block.
|
|
||||||
*
|
|
||||||
* @param key Property name, which must exist on the HTML element. If not already prefixed with
|
|
||||||
* `data-`, `data-` will be prepended to the property.
|
|
||||||
* @returns Value if it exists, `null` if not.
|
|
||||||
*/
|
|
||||||
export function getNetboxData(key: string): string | null {
|
|
||||||
if (!key.startsWith('data-')) {
|
|
||||||
key = `data-${key}`;
|
|
||||||
}
|
|
||||||
for (const element of getElements('body > div#netbox-data > *')) {
|
|
||||||
const value = element.getAttribute(key);
|
|
||||||
if (isTruthy(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle visibility of an element.
|
* Toggle visibility of an element.
|
||||||
*/
|
*/
|
||||||
|
@ -28,6 +28,13 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the bottom margin of <p> elements inside a table cell
|
||||||
|
td > .rendered-markdown {
|
||||||
|
p:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Markdown preview
|
// Markdown preview
|
||||||
.markdown-widget {
|
.markdown-widget {
|
||||||
.preview {
|
.preview {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
// Color labels
|
// Color labels
|
||||||
span.color-label {
|
span.color-label {
|
||||||
display: block;
|
display: inline-block;
|
||||||
width: 5rem;
|
width: 5rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
padding: $badge-padding-y $badge-padding-x;
|
padding: $badge-padding-y $badge-padding-x;
|
||||||
|
@ -9,6 +9,10 @@ pre {
|
|||||||
// Tabler sets display: flex
|
// Tabler sets display: flex
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
.btn-sm {
|
||||||
|
// $border-radius-sm (2px) is too small
|
||||||
|
border-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
|
@ -23,7 +23,6 @@ table.attr-table {
|
|||||||
|
|
||||||
// Restyle row header
|
// Restyle row header
|
||||||
th {
|
th {
|
||||||
color: $gray-700;
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
}
|
}
|
||||||
|
@ -70,10 +70,5 @@
|
|||||||
{# User messages #}
|
{# User messages #}
|
||||||
{% include 'inc/messages.html' %}
|
{% include 'inc/messages.html' %}
|
||||||
|
|
||||||
{# Data container #}
|
|
||||||
<div id="netbox-data" style="display: none!important; visibility: hidden!important">
|
|
||||||
{% block data %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -163,7 +163,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-12 col-xl-7">
|
<div class="col col-12 col-xl-7">
|
||||||
<div class="text-end mb-4">
|
<div class="text-end mb-4">
|
||||||
<select class="btn btn-outline-dark rack-view">
|
<select class="btn btn-outline-secondary no-ts rack-view">
|
||||||
<option value="images-and-labels" selected="selected">{% trans "Images and Labels" %}</option>
|
<option value="images-and-labels" selected="selected">{% trans "Images and Labels" %}</option>
|
||||||
<option value="images-only">{% trans "Images only" %}</option>
|
<option value="images-only">{% trans "Images only" %}</option>
|
||||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||||
|
@ -11,13 +11,11 @@
|
|||||||
<a href="{% url 'dcim:rack_list' %}{% querystring request %}" class="btn btn-primary">
|
<a href="{% url 'dcim:rack_list' %}{% querystring request %}" class="btn btn-primary">
|
||||||
<i class="mdi mdi-format-list-checkbox"></i> {% trans "View List" %}
|
<i class="mdi mdi-format-list-checkbox"></i> {% trans "View List" %}
|
||||||
</a>
|
</a>
|
||||||
<div class="btn-group" role="group">
|
<select class="btn btn-outline-secondary no-ts rack-view">
|
||||||
<select class="btn btn-outline-secondary rack-view">
|
|
||||||
<option value="images-and-labels" selected="selected">{% trans "Images and Labels" %}</option>
|
<option value="images-and-labels" selected="selected">{% trans "Images and Labels" %}</option>
|
||||||
<option value="images-only">{% trans "Images only" %}</option>
|
<option value="images-only">{% trans "Images only" %}</option>
|
||||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">{% trans "Front" %}</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">{% trans "Front" %}</a>
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">{% trans "Rear" %}</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">{% trans "Rear" %}</a>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<div class="toast shadow-sm" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="10000">
|
<div class="toast toast-dark border-0 shadow-sm" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="10000">
|
||||||
<div class="toast-header text-bg-{{ status }}">
|
<div class="toast-header text-bg-{{ status }}">
|
||||||
<i class="mdi mdi-{{ status|icon_from_status }} me-1"></i>
|
<i class="mdi mdi-{{ status|icon_from_status }} me-1"></i>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Users" %}</h5>
|
<h5 class="card-header">{% trans "Users" %}</h5>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for user in object.user_set.all %}
|
{% for user in object.users.all %}
|
||||||
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
|
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
<h5 class="card-header">{% trans "Assigned Groups" %}</h5>
|
<h5 class="card-header">{% trans "Assigned Groups" %}</h5>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for group in object.groups.all %}
|
{% for group in object.groups.all %}
|
||||||
<a href="{% url 'users:netboxgroup' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
|
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
<h5 class="card-header">{% trans "Assigned Groups" %}</h5>
|
<h5 class="card-header">{% trans "Assigned Groups" %}</h5>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for group in object.groups.all %}
|
{% for group in object.groups.all %}
|
||||||
<a href="{% url 'users:netboxgroup' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
|
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
@ -7,7 +6,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
from users.models import ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'NestedGroupSerializer',
|
'NestedGroupSerializer',
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
|
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
from users.models import ObjectPermission
|
from users.models import Group, ObjectPermission
|
||||||
from .users import GroupSerializer, UserSerializer
|
from .users import GroupSerializer, UserSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import SerializedPKRelatedField
|
from netbox.api.fields import SerializedPKRelatedField
|
||||||
from netbox.api.serializers import ValidatedModelSerializer
|
from netbox.api.serializers import ValidatedModelSerializer
|
||||||
|
from users.models import Group
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'GroupSerializer',
|
'GroupSerializer',
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
from django.contrib.auth import authenticate
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from drf_spectacular.utils import extend_schema
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
@ -15,7 +13,7 @@ from rest_framework.viewsets import ViewSet
|
|||||||
|
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from users import filtersets
|
from users import filtersets
|
||||||
from users.models import ObjectPermission, Token, UserConfig
|
from users.models import Group, ObjectPermission, Token, UserConfig
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.utils import deepmerge
|
from utilities.utils import deepmerge
|
||||||
from . import serializers
|
from . import serializers
|
||||||
@ -40,7 +38,7 @@ class UserViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class GroupViewSet(NetBoxModelViewSet):
|
class GroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name')
|
queryset = Group.objects.annotate(user_count=Count('user'))
|
||||||
serializer_class = serializers.GroupSerializer
|
serializer_class = serializers.GroupSerializer
|
||||||
filterset_class = filtersets.GroupFilterSet
|
filterset_class = filtersets.GroupFilterSet
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.filtersets import BaseFilterSet
|
from netbox.filtersets import BaseFilterSet
|
||||||
from users.models import ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'GroupFilterSet',
|
'GroupFilterSet',
|
||||||
|
@ -14,7 +14,7 @@ __all__ = (
|
|||||||
class GroupImportForm(CSVModelForm):
|
class GroupImportForm(CSVModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = NetBoxGroup
|
model = Group
|
||||||
fields = (
|
fields = (
|
||||||
'name',
|
'name',
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from netbox.forms.mixins import SavedFiltersMixin
|
from netbox.forms.mixins import SavedFiltersMixin
|
||||||
from users.models import NetBoxGroup, User, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token, User
|
||||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
|
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
|
||||||
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
from utilities.forms.fields import DynamicModelMultipleChoiceField
|
||||||
from utilities.forms.widgets import DateTimePicker
|
from utilities.forms.widgets import DateTimePicker
|
||||||
@ -19,7 +18,7 @@ __all__ = (
|
|||||||
|
|
||||||
|
|
||||||
class GroupFilterForm(NetBoxModelFilterSetForm):
|
class GroupFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = NetBoxGroup
|
model = Group
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'filter_id',)),
|
(None, ('q', 'filter_id',)),
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.forms import SimpleArrayField
|
from django.contrib.postgres.forms import SimpleArrayField
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
@ -253,7 +252,7 @@ class GroupForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = NetBoxGroup
|
model = Group
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'users', 'object_permissions',
|
'name', 'users', 'object_permissions',
|
||||||
]
|
]
|
||||||
@ -263,14 +262,14 @@ class GroupForm(forms.ModelForm):
|
|||||||
|
|
||||||
# Populate assigned users and permissions
|
# Populate assigned users and permissions
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
self.fields['users'].initial = self.instance.user_set.values_list('id', flat=True)
|
self.fields['users'].initial = self.instance.users.values_list('id', flat=True)
|
||||||
self.fields['object_permissions'].initial = self.instance.object_permissions.values_list('id', flat=True)
|
self.fields['object_permissions'].initial = self.instance.object_permissions.values_list('id', flat=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
instance = super().save(*args, **kwargs)
|
instance = super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Update assigned users and permissions
|
# Update assigned users and permissions
|
||||||
instance.user_set.set(self.cleaned_data['users'])
|
instance.users.set(self.cleaned_data['users'])
|
||||||
instance.object_permissions.set(self.cleaned_data['object_permissions'])
|
instance.object_permissions.set(self.cleaned_data['object_permissions'])
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||||
from .types import *
|
from users.models import Group
|
||||||
from utilities.graphql_optimizer import gql_query_optimizer
|
from utilities.graphql_optimizer import gql_query_optimizer
|
||||||
|
from .types import *
|
||||||
|
|
||||||
|
|
||||||
class UsersQuery(graphene.ObjectType):
|
class UsersQuery(graphene.ObjectType):
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
from users import filtersets
|
from users import filtersets
|
||||||
|
from users.models import Group
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# Generated by Django 5.0.1 on 2024-01-31 23:18
|
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
@ -27,12 +25,26 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
# 0001_squashed had model with db_table=auth_user - now we switch it
|
# The User table was originally created as 'auth_user'. Now we nullify the model's
|
||||||
# to None to use the default Django resolution (users.user)
|
# db_table option, so that it defaults to the app & model name (users_user). This
|
||||||
|
# causes the database table to be renamed.
|
||||||
migrations.AlterModelTable(
|
migrations.AlterModelTable(
|
||||||
name='user',
|
name='user',
|
||||||
table=None,
|
table=None,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# Rename auth_user_* sequences
|
||||||
|
migrations.RunSQL("ALTER TABLE auth_user_groups_id_seq RENAME TO users_user_groups_id_seq"),
|
||||||
|
migrations.RunSQL("ALTER TABLE auth_user_id_seq RENAME TO users_user_id_seq"),
|
||||||
|
migrations.RunSQL("ALTER TABLE auth_user_user_permissions_id_seq RENAME TO users_user_user_permissions_id_seq"),
|
||||||
|
|
||||||
|
# Rename auth_user_* indexes
|
||||||
|
migrations.RunSQL("ALTER INDEX auth_user_pkey RENAME TO users_user_pkey"),
|
||||||
|
# Hash is deterministic; generated via schema_editor._create_index_name()
|
||||||
|
migrations.RunSQL("ALTER INDEX auth_user_username_6821ab7c_like RENAME TO users_user_username_06e46fe6_like"),
|
||||||
|
migrations.RunSQL("ALTER INDEX auth_user_username_key RENAME TO users_user_username_key"),
|
||||||
|
|
||||||
|
# Update ContentTypes
|
||||||
migrations.RunPython(
|
migrations.RunPython(
|
||||||
code=update_content_types,
|
code=update_content_types,
|
||||||
reverse_code=migrations.RunPython.noop
|
reverse_code=migrations.RunPython.noop
|
||||||
|
80
netbox/users/migrations/0006_custom_group_model.py
Normal file
80
netbox/users/migrations/0006_custom_group_model.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import users.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def update_custom_fields(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Update any CustomFields referencing the old Group model to use the new model.
|
||||||
|
"""
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
CustomField = apps.get_model('extras', 'CustomField')
|
||||||
|
Group = apps.get_model('users', 'Group')
|
||||||
|
|
||||||
|
if old_ct := ContentType.objects.filter(app_label='users', model='netboxgroup').first():
|
||||||
|
new_ct = ContentType.objects.get_for_model(Group)
|
||||||
|
CustomField.objects.filter(object_type=old_ct).update(object_type=new_ct)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0005_alter_user_table'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Create the new Group model & table
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=150, unique=True)),
|
||||||
|
('description', models.CharField(blank=True, max_length=200)),
|
||||||
|
('permissions', models.ManyToManyField(blank=True, related_name='groups', related_query_name='group', to='auth.permission')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'group',
|
||||||
|
'verbose_name_plural': 'groups',
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', users.models.NetBoxGroupManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
# Copy existing groups from the old table into the new one
|
||||||
|
migrations.RunSQL(
|
||||||
|
"INSERT INTO users_group (SELECT id, name, '' AS description FROM auth_group)"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Update the sequence for group ID values
|
||||||
|
migrations.RunSQL(
|
||||||
|
"SELECT setval('users_group_id_seq', (SELECT MAX(id) FROM users_group))"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Update the "groups" M2M fields on User & ObjectPermission
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='groups',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='users', related_query_name='user', to='users.group'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objectpermission',
|
||||||
|
name='groups',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='object_permissions', to='users.group'),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Delete groups from the old table
|
||||||
|
migrations.RunSQL(
|
||||||
|
"DELETE from auth_group"
|
||||||
|
),
|
||||||
|
|
||||||
|
# Update custom fields
|
||||||
|
migrations.RunPython(
|
||||||
|
code=update_custom_fields,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
|
||||||
|
# Delete the proxy model
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='NetBoxGroup',
|
||||||
|
),
|
||||||
|
]
|
@ -4,7 +4,12 @@ import os
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
AbstractUser, Group, GroupManager, User as DjangoUser, UserManager as DjangoUserManager
|
AbstractUser,
|
||||||
|
Group as DjangoGroup,
|
||||||
|
GroupManager,
|
||||||
|
Permission,
|
||||||
|
User as DjangoUser,
|
||||||
|
UserManager as DjangoUserManager
|
||||||
)
|
)
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -25,7 +30,7 @@ from utilities.utils import flatten_dict
|
|||||||
from .constants import *
|
from .constants import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'NetBoxGroup',
|
'Group',
|
||||||
'ObjectPermission',
|
'ObjectPermission',
|
||||||
'Token',
|
'Token',
|
||||||
'User',
|
'User',
|
||||||
@ -33,22 +38,61 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
|
||||||
# Proxies for Django's User and Group models
|
pass
|
||||||
#
|
|
||||||
|
|
||||||
|
class Group(models.Model):
|
||||||
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
|
max_length=150,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
|
max_length=200,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replicate legacy Django permissions support from stock Group model
|
||||||
|
# to ensure authentication backend compatibility
|
||||||
|
permissions = models.ManyToManyField(
|
||||||
|
Permission,
|
||||||
|
verbose_name=_("permissions"),
|
||||||
|
blank=True,
|
||||||
|
related_name='groups',
|
||||||
|
related_query_name='group'
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = NetBoxGroupManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('group')
|
||||||
|
verbose_name_plural = _('groups')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('users:group', args=[self.pk])
|
||||||
|
|
||||||
|
def natural_key(self):
|
||||||
|
return (self.name,)
|
||||||
|
|
||||||
|
|
||||||
class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)):
|
class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
"""
|
groups = models.ManyToManyField(
|
||||||
Proxy contrib.auth.models.User for the UI
|
to='users.Group',
|
||||||
"""
|
verbose_name=_('groups'),
|
||||||
|
blank=True,
|
||||||
|
related_name='users',
|
||||||
|
related_query_name='user'
|
||||||
|
)
|
||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -68,22 +112,6 @@ class User(AbstractUser):
|
|||||||
raise ValidationError(_("A user with this username already exists."))
|
raise ValidationError(_("A user with this username already exists."))
|
||||||
|
|
||||||
|
|
||||||
class NetBoxGroup(Group):
|
|
||||||
"""
|
|
||||||
Proxy contrib.auth.models.User for the UI
|
|
||||||
"""
|
|
||||||
objects = NetBoxGroupManager()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
proxy = True
|
|
||||||
ordering = ('name',)
|
|
||||||
verbose_name = _('group')
|
|
||||||
verbose_name_plural = _('groups')
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('users:netboxgroup', args=[self.pk])
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# User preferences
|
# User preferences
|
||||||
#
|
#
|
||||||
@ -360,7 +388,7 @@ class ObjectPermission(models.Model):
|
|||||||
related_name='object_permissions'
|
related_name='object_permissions'
|
||||||
)
|
)
|
||||||
groups = models.ManyToManyField(
|
groups = models.ManyToManyField(
|
||||||
to=Group,
|
to='users.Group',
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='object_permissions'
|
related_name='object_permissions'
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
|
|||||||
|
|
||||||
from account.tables import UserTokenTable
|
from account.tables import UserTokenTable
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from users.models import NetBoxGroup, User, ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token, User
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'GroupTable',
|
'GroupTable',
|
||||||
@ -33,7 +33,7 @@ class UserTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
groups = columns.ManyToManyColumn(
|
groups = columns.ManyToManyColumn(
|
||||||
verbose_name=_('Groups'),
|
verbose_name=_('Groups'),
|
||||||
linkify_item=('users:netboxgroup', {'pk': tables.A('pk')})
|
linkify_item=('users:group', {'pk': tables.A('pk')})
|
||||||
)
|
)
|
||||||
is_active = columns.BooleanColumn(
|
is_active = columns.BooleanColumn(
|
||||||
verbose_name=_('Is Active'),
|
verbose_name=_('Is Active'),
|
||||||
@ -67,7 +67,7 @@ class GroupTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = NetBoxGroup
|
model = Group
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'users_count',
|
'pk', 'id', 'name', 'users_count',
|
||||||
)
|
)
|
||||||
@ -107,7 +107,7 @@ class ObjectPermissionTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
groups = columns.ManyToManyColumn(
|
groups = columns.ManyToManyColumn(
|
||||||
verbose_name=_('Groups'),
|
verbose_name=_('Groups'),
|
||||||
linkify_item=('users:netboxgroup', {'pk': tables.A('pk')})
|
linkify_item=('users:group', {'pk': tables.A('pk')})
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
actions=('edit', 'delete'),
|
actions=('edit', 'delete'),
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from users.models import ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
from utilities.testing import APIViewTestCases, APITestCase, create_test_user
|
from utilities.testing import APIViewTestCases, APITestCase, create_test_user
|
||||||
from utilities.utils import deepmerge
|
from utilities.utils import deepmerge
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
|
|
||||||
from users import filtersets
|
from users import filtersets
|
||||||
from users.models import ObjectPermission, Token
|
from users.models import Group, ObjectPermission, Token
|
||||||
from utilities.testing import BaseFilterSetTests
|
from utilities.testing import BaseFilterSetTests
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.auth.models import Group
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from users.models import *
|
from users.models import *
|
||||||
@ -70,7 +69,7 @@ class GroupTestCase(
|
|||||||
ViewTestCases.BulkImportObjectsViewTestCase,
|
ViewTestCases.BulkImportObjectsViewTestCase,
|
||||||
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
||||||
):
|
):
|
||||||
model = NetBoxGroup
|
model = Group
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -23,11 +23,11 @@ urlpatterns = [
|
|||||||
path('users/<int:pk>/', include(get_model_urls('users', 'user'))),
|
path('users/<int:pk>/', include(get_model_urls('users', 'user'))),
|
||||||
|
|
||||||
# Groups
|
# Groups
|
||||||
path('groups/', views.GroupListView.as_view(), name='netboxgroup_list'),
|
path('groups/', views.GroupListView.as_view(), name='group_list'),
|
||||||
path('groups/add/', views.GroupEditView.as_view(), name='netboxgroup_add'),
|
path('groups/add/', views.GroupEditView.as_view(), name='group_add'),
|
||||||
path('groups/import/', views.GroupBulkImportView.as_view(), name='netboxgroup_import'),
|
path('groups/import/', views.GroupBulkImportView.as_view(), name='group_import'),
|
||||||
path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='netboxgroup_bulk_delete'),
|
path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='group_bulk_delete'),
|
||||||
path('groups/<int:pk>/', include(get_model_urls('users', 'netboxgroup'))),
|
path('groups/<int:pk>/', include(get_model_urls('users', 'group'))),
|
||||||
|
|
||||||
# Permissions
|
# Permissions
|
||||||
path('permissions/', views.ObjectPermissionListView.as_view(), name='objectpermission_list'),
|
path('permissions/', views.ObjectPermissionListView.as_view(), name='objectpermission_list'),
|
||||||
|
@ -5,7 +5,7 @@ from extras.tables import ObjectChangeTable
|
|||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.views import register_model_view
|
from utilities.views import register_model_view
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .models import NetBoxGroup, User, ObjectPermission, Token
|
from .models import Group, User, ObjectPermission, Token
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -110,36 +110,36 @@ class UserBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class GroupListView(generic.ObjectListView):
|
class GroupListView(generic.ObjectListView):
|
||||||
queryset = NetBoxGroup.objects.annotate(users_count=Count('user'))
|
queryset = Group.objects.annotate(users_count=Count('user'))
|
||||||
filterset = filtersets.GroupFilterSet
|
filterset = filtersets.GroupFilterSet
|
||||||
filterset_form = forms.GroupFilterForm
|
filterset_form = forms.GroupFilterForm
|
||||||
table = tables.GroupTable
|
table = tables.GroupTable
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(NetBoxGroup)
|
@register_model_view(Group)
|
||||||
class GroupView(generic.ObjectView):
|
class GroupView(generic.ObjectView):
|
||||||
queryset = NetBoxGroup.objects.all()
|
queryset = Group.objects.all()
|
||||||
template_name = 'users/group.html'
|
template_name = 'users/group.html'
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(NetBoxGroup, 'edit')
|
@register_model_view(Group, 'edit')
|
||||||
class GroupEditView(generic.ObjectEditView):
|
class GroupEditView(generic.ObjectEditView):
|
||||||
queryset = NetBoxGroup.objects.all()
|
queryset = Group.objects.all()
|
||||||
form = forms.GroupForm
|
form = forms.GroupForm
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(NetBoxGroup, 'delete')
|
@register_model_view(Group, 'delete')
|
||||||
class GroupDeleteView(generic.ObjectDeleteView):
|
class GroupDeleteView(generic.ObjectDeleteView):
|
||||||
queryset = NetBoxGroup.objects.all()
|
queryset = Group.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class GroupBulkImportView(generic.BulkImportView):
|
class GroupBulkImportView(generic.BulkImportView):
|
||||||
queryset = NetBoxGroup.objects.all()
|
queryset = Group.objects.all()
|
||||||
model_form = forms.GroupImportForm
|
model_form = forms.GroupImportForm
|
||||||
|
|
||||||
|
|
||||||
class GroupBulkDeleteView(generic.BulkDeleteView):
|
class GroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = NetBoxGroup.objects.annotate(users_count=Count('user'))
|
queryset = Group.objects.annotate(users_count=Count('user'))
|
||||||
filterset = filtersets.GroupFilterSet
|
filterset = filtersets.GroupFilterSet
|
||||||
table = tables.GroupTable
|
table = tables.GroupTable
|
||||||
|
|
||||||
|
@ -34,23 +34,13 @@ def get_serializer_for_model(model, prefix=''):
|
|||||||
"""
|
"""
|
||||||
Dynamically resolve and return the appropriate serializer for a model.
|
Dynamically resolve and return the appropriate serializer for a model.
|
||||||
"""
|
"""
|
||||||
app_name, model_name = model._meta.label.split('.')
|
app_label, model_name = model._meta.label.split('.')
|
||||||
# Serializers for Django's auth models are in the users app
|
serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer'
|
||||||
if app_name == 'auth':
|
|
||||||
app_name = 'users'
|
|
||||||
# Account for changes using Proxy model
|
|
||||||
if app_name == 'users':
|
|
||||||
if model_name == 'NetBoxUser':
|
|
||||||
model_name = 'User'
|
|
||||||
elif model_name == 'NetBoxGroup':
|
|
||||||
model_name = 'Group'
|
|
||||||
|
|
||||||
serializer_name = f'{app_name}.api.serializers.{prefix}{model_name}Serializer'
|
|
||||||
try:
|
try:
|
||||||
return dynamic_import(serializer_name)
|
return dynamic_import(serializer_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise SerializerNotFound(
|
raise SerializerNotFound(
|
||||||
f"Could not determine serializer for {app_name}.{model_name} with prefix '{prefix}'"
|
f"Could not determine serializer for {app_label}.{model_name} with prefix '{prefix}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
{% elif 'data-clipboard' in field.field.widget.attrs %}
|
{% elif 'data-clipboard' in field.field.widget.attrs %}
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
{{ field }}
|
{{ field }}
|
||||||
<button type="button" title="{% trans "Copy to clipboard" %}" class="btn btn-outline-dark copy-content" data-clipboard-target="#{{ field.id_for_label }}">
|
<button type="button" title="{% trans "Copy to clipboard" %}" class="btn copy-content" data-clipboard-target="#{{ field.id_for_label }}">
|
||||||
<i class="mdi mdi-content-copy"></i>
|
<i class="mdi mdi-content-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<div class="dropdown-menu-columns">
|
<div class="dropdown-menu-columns">
|
||||||
<div class="dropdown-menu-column pb-2">
|
<div class="dropdown-menu-column pb-2">
|
||||||
{% for group, items in groups %}
|
{% for group, items in groups %}
|
||||||
<div class="text-uppercase fw-bold fs-5 ps-3 pt-3 pb-1">
|
<div class="text-uppercase text-secondary fw-bold fs-5 ps-3 pt-3 pb-1">
|
||||||
{{ group.label }}
|
{{ group.label }}
|
||||||
</div>
|
</div>
|
||||||
{% for item, buttons in items %}
|
{% for item, buttons in items %}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
{% include 'django/forms/widgets/number.html' %}
|
{% include 'django/forms/widgets/number.html' %}
|
||||||
<button type="button" class="btn btn-outline-dark dropdown-toggle" data-bs-toggle="dropdown"></button>
|
<button type="button" class="btn" data-bs-toggle="dropdown">
|
||||||
|
<i class="mdi mdi-chevron-down"></i>
|
||||||
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
{% for value, label in widget.options %}
|
{% for value, label in widget.options %}
|
||||||
<li>
|
<li>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import json
|
import json
|
||||||
import nh3
|
|
||||||
import re
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from itertools import count, groupby
|
from itertools import count, groupby
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import nh3
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.db.models import Count, ManyToOneRel, OuterRef, Subquery
|
from django.db.models import Count, ManyToOneRel, OuterRef, Subquery
|
||||||
@ -23,7 +24,6 @@ from dcim.choices import CableLengthUnitChoices, WeightUnitChoices
|
|||||||
from extras.utils import is_taggable
|
from extras.utils import is_taggable
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
from urllib.parse import urlencode
|
|
||||||
from utilities.constants import HTTP_REQUEST_META_SAFE_COPY
|
from utilities.constants import HTTP_REQUEST_META_SAFE_COPY
|
||||||
from .constants import HTML_ALLOWED_ATTRIBUTES, HTML_ALLOWED_TAGS
|
from .constants import HTML_ALLOWED_ATTRIBUTES, HTML_ALLOWED_TAGS
|
||||||
|
|
||||||
@ -48,26 +48,16 @@ def get_viewname(model, action=None, rest_api=False):
|
|||||||
model_name = model._meta.model_name
|
model_name = model._meta.model_name
|
||||||
|
|
||||||
if rest_api:
|
if rest_api:
|
||||||
if is_plugin:
|
|
||||||
viewname = f'plugins-api:{app_label}-api:{model_name}'
|
|
||||||
else:
|
|
||||||
# Alter the app_label for group and user model_name to point to users app
|
|
||||||
if app_label == 'auth' and model_name in ['group', 'user']:
|
|
||||||
app_label = 'users'
|
|
||||||
if app_label == 'users' and model._meta.proxy and model_name in ['netboxuser', 'netboxgroup']:
|
|
||||||
model_name = model._meta.proxy_for_model._meta.model_name
|
|
||||||
|
|
||||||
viewname = f'{app_label}-api:{model_name}'
|
viewname = f'{app_label}-api:{model_name}'
|
||||||
# Append the action, if any
|
if is_plugin:
|
||||||
|
viewname = f'plugins-api:{viewname}'
|
||||||
if action:
|
if action:
|
||||||
viewname = f'{viewname}-{action}'
|
viewname = f'{viewname}-{action}'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
viewname = f'{app_label}:{model_name}'
|
viewname = f'{app_label}:{model_name}'
|
||||||
# Prepend the plugins namespace if this is a plugin model
|
|
||||||
if is_plugin:
|
if is_plugin:
|
||||||
viewname = f'plugins:{viewname}'
|
viewname = f'plugins:{viewname}'
|
||||||
# Append the action, if any
|
|
||||||
if action:
|
if action:
|
||||||
viewname = f'{viewname}_{action}'
|
viewname = f'{viewname}_{action}'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user