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

Merge branch 'feature' into 15277-object-types

This commit is contained in:
Jeremy Stretch
2024-03-05 08:52:07 -05:00
103 changed files with 4252 additions and 3691 deletions

View File

@@ -3,23 +3,26 @@ import sys
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import FieldDoesNotExist
from django.core.exceptions import (
FieldDoesNotExist, FieldError, MultipleObjectsReturned, ObjectDoesNotExist, ValidationError,
)
from django.db.models.fields.related import ManyToOneRel, RelatedField
from django.http import JsonResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework import status
from rest_framework.serializers import Serializer
from rest_framework.utils import formatting
from netbox.api.fields import RelatedObjectCountField
from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
from utilities.utils import count_related
from .utils import dynamic_import
from .utils import count_related, dict_to_filter_params, dynamic_import
__all__ = (
'get_annotations_for_serializer',
'get_graphql_type_for_model',
'get_prefetches_for_serializer',
'get_related_object_by_attrs',
'get_serializer_for_model',
'get_view_name',
'is_api_request',
@@ -93,7 +96,7 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
"""
model = serializer_class.Meta.model
# If specific fields are not specified, default to all
# If fields are not specified, default to all
if not fields_to_include:
fields_to_include = serializer_class.Meta.fields
@@ -118,7 +121,9 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
# for the related object.
if serializer_field:
if issubclass(type(serializer_field), Serializer):
for subfield in get_prefetches_for_serializer(type(serializer_field)):
# Determine which fields to prefetch for the nested object
subfields = serializer_field.Meta.brief_fields if serializer_field.nested else None
for subfield in get_prefetches_for_serializer(type(serializer_field), subfields):
prefetch_fields.append(f'{field_name}__{subfield}')
return prefetch_fields
@@ -144,6 +149,48 @@ def get_annotations_for_serializer(serializer_class, fields_to_include=None):
return annotations
def get_related_object_by_attrs(queryset, attrs):
"""
Return an object identified by either a dictionary of attributes or its numeric primary key (ID). This is used
for referencing related objects when creating/updating objects via the REST API.
"""
if attrs is None:
return None
# Dictionary of related object attributes
if isinstance(attrs, dict):
params = dict_to_filter_params(attrs)
try:
return queryset.get(**params)
except ObjectDoesNotExist:
raise ValidationError(
_("Related object not found using the provided attributes: {params}").format(params=params))
except MultipleObjectsReturned:
raise ValidationError(
_("Multiple objects match the provided attributes: {params}").format(params=params)
)
except FieldError as e:
raise ValidationError(e)
# Integer PK of related object
try:
# Cast as integer in case a PK was mistakenly sent as a string
pk = int(attrs)
except (TypeError, ValueError):
raise ValidationError(
_(
"Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
"unrecognized value: {value}"
).format(value=attrs)
)
# Look up object by PK
try:
return queryset.get(pk=pk)
except ObjectDoesNotExist:
raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk))
def rest_api_server_error(request, *args, **kwargs):
"""
Handle exceptions and return a useful error message for REST API requests.

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>