mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 04:32:51 -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}',
|
||||
site=site,
|
||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||
device_role=switch_role
|
||||
role=switch_role
|
||||
)
|
||||
switch.full_clean()
|
||||
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.
|
||||
|
||||
### Device Role
|
||||
### Role
|
||||
|
||||
The functional [role](./devicerole.md) assigned to this device.
|
||||
The functional [device role](./devicerole.md) assigned to this device.
|
||||
|
||||
### Device Type
|
||||
|
||||
|
@ -13,6 +13,10 @@
|
||||
|
||||
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
|
||||
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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
|
||||
|
||||
@ -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`
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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')
|
||||
device_type = DeviceTypeSerializer(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(
|
||||
nested=True,
|
||||
required=False,
|
||||
@ -83,13 +78,13 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
||||
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
|
||||
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
|
||||
'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
|
||||
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||
'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count',
|
||||
'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count',
|
||||
'module_bay_count', 'inventory_item_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
@ -104,22 +99,19 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
||||
return data
|
||||
|
||||
def get_device_role(self, obj):
|
||||
return obj.role
|
||||
|
||||
|
||||
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
config_context = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
|
||||
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
|
||||
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
|
||||
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
|
||||
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
|
||||
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
|
||||
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
|
||||
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
|
||||
'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
|
||||
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
|
||||
'device_bay_count', 'module_bay_count', 'inventory_item_count',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
|
@ -815,20 +815,6 @@ class Device(
|
||||
def get_absolute_url(self):
|
||||
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):
|
||||
super().clean()
|
||||
|
||||
|
@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase):
|
||||
device = Device.objects.create(
|
||||
site=self.site,
|
||||
device_type=self.device.device_type,
|
||||
device_role=self.device.device_role,
|
||||
role=self.device.role,
|
||||
name='Test mid-span Device'
|
||||
)
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
|
@ -533,30 +533,6 @@ class DeviceTestCase(TestCase):
|
||||
device2.full_clean()
|
||||
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):
|
||||
|
||||
|
@ -4,13 +4,13 @@ from collections import defaultdict
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
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.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from users.constants import CONSTRAINT_TOKEN_USER
|
||||
from users.models import ObjectPermission
|
||||
from users.models import Group, ObjectPermission
|
||||
from utilities.permissions import (
|
||||
permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct,
|
||||
)
|
||||
|
@ -392,19 +392,19 @@ ADMIN_MENU = Menu(
|
||||
),
|
||||
# Proxy model for auth.Group
|
||||
MenuItem(
|
||||
link=f'users:netboxgroup_list',
|
||||
link=f'users:group_list',
|
||||
link_text=_('Groups'),
|
||||
permissions=[f'auth.view_group'],
|
||||
staff_only=True,
|
||||
buttons=(
|
||||
MenuItemButton(
|
||||
link=f'users:netboxgroup_add',
|
||||
link=f'users:group_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=[f'auth.add_group']
|
||||
),
|
||||
MenuItemButton(
|
||||
link=f'users:netboxgroup_import',
|
||||
link=f'users:group_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
permissions=[f'auth.add_group']
|
||||
|
@ -2,7 +2,6 @@ import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import Client
|
||||
from django.test.utils import override_settings
|
||||
@ -12,7 +11,7 @@ from rest_framework.test import APIClient
|
||||
|
||||
from dcim.models import Site
|
||||
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.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');
|
||||
|
||||
//@ts-ignore
|
||||
target.slim.setData([
|
||||
{text: label, value: value}
|
||||
]);
|
||||
const change = new Event('change');
|
||||
target.dispatchEvent(change);
|
||||
target.tomselect.addOption({
|
||||
id: value,
|
||||
display: label,
|
||||
});
|
||||
//@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
|
||||
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, {
|
||||
...config,
|
||||
valueField: VALUE_FIELD,
|
||||
|
@ -7,7 +7,7 @@ import { getElements } from '../util';
|
||||
// Initialize <select> elements with statically-defined options
|
||||
export function initStaticSelects(): void {
|
||||
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, {
|
||||
...config,
|
||||
@ -24,7 +24,7 @@ export function initColorSelects(): void {
|
||||
)}"></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, {
|
||||
...config,
|
||||
maxOptions: undefined,
|
||||
|
@ -244,29 +244,6 @@ export function getSelectedOptions<E extends HTMLElement>(
|
||||
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.
|
||||
*/
|
||||
|
@ -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-widget {
|
||||
.preview {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
// Color labels
|
||||
span.color-label {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
width: 5rem;
|
||||
height: 1rem;
|
||||
padding: $badge-padding-y $badge-padding-x;
|
||||
|
@ -9,6 +9,10 @@ pre {
|
||||
// Tabler sets display: flex
|
||||
display: inline-block;
|
||||
}
|
||||
.btn-sm {
|
||||
// $border-radius-sm (2px) is too small
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
// Tabs
|
||||
.nav-tabs {
|
||||
|
@ -23,7 +23,6 @@ table.attr-table {
|
||||
|
||||
// Restyle row header
|
||||
th {
|
||||
color: $gray-700;
|
||||
font-weight: normal;
|
||||
width: min-content;
|
||||
}
|
||||
|
@ -70,10 +70,5 @@
|
||||
{# User messages #}
|
||||
{% include 'inc/messages.html' %}
|
||||
|
||||
{# Data container #}
|
||||
<div id="netbox-data" style="display: none!important; visibility: hidden!important">
|
||||
{% block data %}{% endblock %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -163,7 +163,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-xl-7">
|
||||
<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-only">{% trans "Images 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">
|
||||
<i class="mdi mdi-format-list-checkbox"></i> {% trans "View List" %}
|
||||
</a>
|
||||
<div class="btn-group" role="group">
|
||||
<select class="btn btn-outline-secondary 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-only">{% trans "Images only" %}</option>
|
||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<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='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">{% trans "Rear" %}</a>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% 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 }}">
|
||||
<i class="mdi mdi-{{ status|icon_from_status }} me-1"></i>
|
||||
{{ title }}
|
||||
|
@ -24,7 +24,7 @@
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Users" %}</h5>
|
||||
<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>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
|
@ -82,7 +82,7 @@
|
||||
<h5 class="card-header">{% trans "Assigned Groups" %}</h5>
|
||||
<div class="list-group list-group-flush">
|
||||
{% 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 %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
|
@ -53,7 +53,7 @@
|
||||
<h5 class="card-header">{% trans "Assigned Groups" %}</h5>
|
||||
<div class="list-group list-group-flush">
|
||||
{% 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 %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
|
@ -1,5 +1,4 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
@ -7,7 +6,7 @@ from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import ContentTypeField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from users.models import ObjectPermission, Token
|
||||
from users.models import Group, ObjectPermission, Token
|
||||
|
||||
__all__ = [
|
||||
'NestedGroupSerializer',
|
||||
|
@ -1,11 +1,10 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import ValidatedModelSerializer
|
||||
from users.models import ObjectPermission
|
||||
from users.models import Group, ObjectPermission
|
||||
from .users import GroupSerializer, UserSerializer
|
||||
|
||||
__all__ = (
|
||||
|
@ -1,11 +1,11 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import SerializedPKRelatedField
|
||||
from netbox.api.serializers import ValidatedModelSerializer
|
||||
from users.models import Group
|
||||
|
||||
__all__ = (
|
||||
'GroupSerializer',
|
||||
|
@ -1,11 +1,9 @@
|
||||
import logging
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db.models import Count
|
||||
from drf_spectacular.utils import extend_schema
|
||||
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.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
@ -15,7 +13,7 @@ from rest_framework.viewsets import ViewSet
|
||||
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
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.utils import deepmerge
|
||||
from . import serializers
|
||||
@ -40,7 +38,7 @@ class UserViewSet(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
|
||||
filterset_class = filtersets.GroupFilterSet
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import django_filters
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.filtersets import BaseFilterSet
|
||||
from users.models import ObjectPermission, Token
|
||||
from users.models import Group, ObjectPermission, Token
|
||||
|
||||
__all__ = (
|
||||
'GroupFilterSet',
|
||||
|
@ -14,7 +14,7 @@ __all__ = (
|
||||
class GroupImportForm(CSVModelForm):
|
||||
|
||||
class Meta:
|
||||
model = NetBoxGroup
|
||||
model = Group
|
||||
fields = (
|
||||
'name',
|
||||
)
|
||||
|
@ -1,11 +1,10 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
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.fields import DynamicModelMultipleChoiceField
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
@ -19,7 +18,7 @@ __all__ = (
|
||||
|
||||
|
||||
class GroupFilterForm(NetBoxModelFilterSetForm):
|
||||
model = NetBoxGroup
|
||||
model = Group
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id',)),
|
||||
)
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
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.postgres.forms import SimpleArrayField
|
||||
from django.core.exceptions import FieldError
|
||||
@ -253,7 +252,7 @@ class GroupForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = NetBoxGroup
|
||||
model = Group
|
||||
fields = [
|
||||
'name', 'users', 'object_permissions',
|
||||
]
|
||||
@ -263,14 +262,14 @@ class GroupForm(forms.ModelForm):
|
||||
|
||||
# Populate assigned users and permissions
|
||||
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)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
||||
# 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'])
|
||||
|
||||
return instance
|
||||
|
@ -1,10 +1,10 @@
|
||||
import graphene
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||
from .types import *
|
||||
from users.models import Group
|
||||
from utilities.graphql_optimizer import gql_query_optimizer
|
||||
from .types import *
|
||||
|
||||
|
||||
class UsersQuery(graphene.ObjectType):
|
||||
|
@ -1,8 +1,8 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from users import filtersets
|
||||
from users.models import Group
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
|
@ -1,5 +1,3 @@
|
||||
# Generated by Django 5.0.1 on 2024-01-31 23:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@ -27,12 +25,26 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
# 0001_squashed had model with db_table=auth_user - now we switch it
|
||||
# to None to use the default Django resolution (users.user)
|
||||
# The User table was originally created as 'auth_user'. Now we nullify the model's
|
||||
# db_table option, so that it defaults to the app & model name (users_user). This
|
||||
# causes the database table to be renamed.
|
||||
migrations.AlterModelTable(
|
||||
name='user',
|
||||
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(
|
||||
code=update_content_types,
|
||||
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.contrib.auth import get_user_model
|
||||
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.core.exceptions import ValidationError
|
||||
@ -25,7 +30,7 @@ from utilities.utils import flatten_dict
|
||||
from .constants import *
|
||||
|
||||
__all__ = (
|
||||
'NetBoxGroup',
|
||||
'Group',
|
||||
'ObjectPermission',
|
||||
'Token',
|
||||
'User',
|
||||
@ -33,22 +38,61 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Proxies for Django's User and Group models
|
||||
#
|
||||
class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
|
||||
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)):
|
||||
pass
|
||||
|
||||
|
||||
class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
|
||||
pass
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Proxy contrib.auth.models.User for the UI
|
||||
"""
|
||||
groups = models.ManyToManyField(
|
||||
to='users.Group',
|
||||
verbose_name=_('groups'),
|
||||
blank=True,
|
||||
related_name='users',
|
||||
related_query_name='user'
|
||||
)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
class Meta:
|
||||
@ -68,22 +112,6 @@ class User(AbstractUser):
|
||||
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
|
||||
#
|
||||
@ -360,7 +388,7 @@ class ObjectPermission(models.Model):
|
||||
related_name='object_permissions'
|
||||
)
|
||||
groups = models.ManyToManyField(
|
||||
to=Group,
|
||||
to='users.Group',
|
||||
blank=True,
|
||||
related_name='object_permissions'
|
||||
)
|
||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from account.tables import UserTokenTable
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from users.models import NetBoxGroup, User, ObjectPermission, Token
|
||||
from users.models import Group, ObjectPermission, Token, User
|
||||
|
||||
__all__ = (
|
||||
'GroupTable',
|
||||
@ -33,7 +33,7 @@ class UserTable(NetBoxTable):
|
||||
)
|
||||
groups = columns.ManyToManyColumn(
|
||||
verbose_name=_('Groups'),
|
||||
linkify_item=('users:netboxgroup', {'pk': tables.A('pk')})
|
||||
linkify_item=('users:group', {'pk': tables.A('pk')})
|
||||
)
|
||||
is_active = columns.BooleanColumn(
|
||||
verbose_name=_('Is Active'),
|
||||
@ -67,7 +67,7 @@ class GroupTable(NetBoxTable):
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = NetBoxGroup
|
||||
model = Group
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'users_count',
|
||||
)
|
||||
@ -107,7 +107,7 @@ class ObjectPermissionTable(NetBoxTable):
|
||||
)
|
||||
groups = columns.ManyToManyColumn(
|
||||
verbose_name=_('Groups'),
|
||||
linkify_item=('users:netboxgroup', {'pk': tables.A('pk')})
|
||||
linkify_item=('users:group', {'pk': tables.A('pk')})
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('edit', 'delete'),
|
||||
|
@ -1,9 +1,8 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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.utils import deepmerge
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
from users import filtersets
|
||||
from users.models import ObjectPermission, Token
|
||||
from users.models import Group, ObjectPermission, Token
|
||||
from utilities.testing import BaseFilterSetTests
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -1,4 +1,3 @@
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from users.models import *
|
||||
@ -70,7 +69,7 @@ class GroupTestCase(
|
||||
ViewTestCases.BulkImportObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase,
|
||||
):
|
||||
model = NetBoxGroup
|
||||
model = Group
|
||||
maxDiff = None
|
||||
|
||||
@classmethod
|
||||
|
@ -23,11 +23,11 @@ urlpatterns = [
|
||||
path('users/<int:pk>/', include(get_model_urls('users', 'user'))),
|
||||
|
||||
# Groups
|
||||
path('groups/', views.GroupListView.as_view(), name='netboxgroup_list'),
|
||||
path('groups/add/', views.GroupEditView.as_view(), name='netboxgroup_add'),
|
||||
path('groups/import/', views.GroupBulkImportView.as_view(), name='netboxgroup_import'),
|
||||
path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='netboxgroup_bulk_delete'),
|
||||
path('groups/<int:pk>/', include(get_model_urls('users', 'netboxgroup'))),
|
||||
path('groups/', views.GroupListView.as_view(), name='group_list'),
|
||||
path('groups/add/', views.GroupEditView.as_view(), name='group_add'),
|
||||
path('groups/import/', views.GroupBulkImportView.as_view(), name='group_import'),
|
||||
path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='group_bulk_delete'),
|
||||
path('groups/<int:pk>/', include(get_model_urls('users', 'group'))),
|
||||
|
||||
# Permissions
|
||||
path('permissions/', views.ObjectPermissionListView.as_view(), name='objectpermission_list'),
|
||||
|
@ -5,7 +5,7 @@ from extras.tables import ObjectChangeTable
|
||||
from netbox.views import generic
|
||||
from utilities.views import register_model_view
|
||||
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):
|
||||
queryset = NetBoxGroup.objects.annotate(users_count=Count('user'))
|
||||
queryset = Group.objects.annotate(users_count=Count('user'))
|
||||
filterset = filtersets.GroupFilterSet
|
||||
filterset_form = forms.GroupFilterForm
|
||||
table = tables.GroupTable
|
||||
|
||||
|
||||
@register_model_view(NetBoxGroup)
|
||||
@register_model_view(Group)
|
||||
class GroupView(generic.ObjectView):
|
||||
queryset = NetBoxGroup.objects.all()
|
||||
queryset = Group.objects.all()
|
||||
template_name = 'users/group.html'
|
||||
|
||||
|
||||
@register_model_view(NetBoxGroup, 'edit')
|
||||
@register_model_view(Group, 'edit')
|
||||
class GroupEditView(generic.ObjectEditView):
|
||||
queryset = NetBoxGroup.objects.all()
|
||||
queryset = Group.objects.all()
|
||||
form = forms.GroupForm
|
||||
|
||||
|
||||
@register_model_view(NetBoxGroup, 'delete')
|
||||
@register_model_view(Group, 'delete')
|
||||
class GroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = NetBoxGroup.objects.all()
|
||||
queryset = Group.objects.all()
|
||||
|
||||
|
||||
class GroupBulkImportView(generic.BulkImportView):
|
||||
queryset = NetBoxGroup.objects.all()
|
||||
queryset = Group.objects.all()
|
||||
model_form = forms.GroupImportForm
|
||||
|
||||
|
||||
class GroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = NetBoxGroup.objects.annotate(users_count=Count('user'))
|
||||
queryset = Group.objects.annotate(users_count=Count('user'))
|
||||
filterset = filtersets.GroupFilterSet
|
||||
table = tables.GroupTable
|
||||
|
||||
|
@ -34,23 +34,13 @@ def get_serializer_for_model(model, prefix=''):
|
||||
"""
|
||||
Dynamically resolve and return the appropriate serializer for a model.
|
||||
"""
|
||||
app_name, model_name = model._meta.label.split('.')
|
||||
# Serializers for Django's auth models are in the users app
|
||||
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'
|
||||
app_label, model_name = model._meta.label.split('.')
|
||||
serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer'
|
||||
try:
|
||||
return dynamic_import(serializer_name)
|
||||
except AttributeError:
|
||||
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 %}
|
||||
<div class="input-group">
|
||||
{{ 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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<div class="dropdown-menu-columns">
|
||||
<div class="dropdown-menu-column pb-2">
|
||||
{% 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 }}
|
||||
</div>
|
||||
{% for item, buttons in items %}
|
||||
|
@ -1,6 +1,8 @@
|
||||
<div class="input-group">
|
||||
{% 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">
|
||||
{% for value, label in widget.options %}
|
||||
<li>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import datetime
|
||||
import decimal
|
||||
import json
|
||||
import nh3
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from itertools import count, groupby
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import nh3
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core import serializers
|
||||
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 netbox.config import get_config
|
||||
from netbox.plugins import PluginConfig
|
||||
from urllib.parse import urlencode
|
||||
from utilities.constants import HTTP_REQUEST_META_SAFE_COPY
|
||||
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
|
||||
|
||||
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}'
|
||||
# Append the action, if any
|
||||
if is_plugin:
|
||||
viewname = f'plugins-api:{viewname}'
|
||||
if action:
|
||||
viewname = f'{viewname}-{action}'
|
||||
|
||||
else:
|
||||
viewname = f'{app_label}:{model_name}'
|
||||
# Prepend the plugins namespace if this is a plugin model
|
||||
if is_plugin:
|
||||
viewname = f'plugins:{viewname}'
|
||||
# Append the action, if any
|
||||
if action:
|
||||
viewname = f'{viewname}_{action}'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user