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
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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">
|
||||
<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>
|
||||
<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 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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
viewname = f'{app_label}-api:{model_name}'
|
||||
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
|
||||
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