diff --git a/docs/_theme/main.html b/docs/_theme/main.html index 3ff44b9cb..4dfc4e14e 100644 --- a/docs/_theme/main.html +++ b/docs/_theme/main.html @@ -2,8 +2,8 @@ {% block site_meta %} {{ super() }} - {# Disable search indexing unless we're building for ReadTheDocs (see #10496) #} - {% if page.canonical_url != 'https://docs.netbox.dev/' %} + {# Disable search indexing unless we're building for ReadTheDocs #} + {% if not config.extra.readthedocs %} {% endif %} {% endblock %} diff --git a/docs/configuration/system.md b/docs/configuration/system.md index 806839778..28c09444b 100644 --- a/docs/configuration/system.md +++ b/docs/configuration/system.md @@ -16,10 +16,7 @@ BASE_PATH = 'netbox/' Default: `en-us` (US English) -Defines the default preferred language/locale for requests that do not specify one. This is used to alter e.g. the display of dates and numbers to fit the user's locale. See [this list](http://www.i18nguy.com/unicode/language-identifiers.html) of standard language codes. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.) - -!!! note - Altering this parameter will *not* change the language used in NetBox. We hope to provide translation support in a future NetBox release. +Defines the default preferred language/locale for requests that do not specify one. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.) --- diff --git a/mkdocs.yml b/mkdocs.yml index c04ef519f..5aa657230 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,7 @@ plugins: show_root_toc_entry: false show_source: false extra: + readthedocs: !ENV READTHEDOCS social: - icon: fontawesome/brands/github link: https://github.com/netbox-community/netbox diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index ae891dd59..0f4f971dc 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -3,6 +3,7 @@ import json from django import forms from django.conf import settings +from django.forms.fields import JSONField as _JSONField from django.utils.translation import gettext_lazy as _ from core.forms.mixins import SyncedDataMixin @@ -12,7 +13,7 @@ from netbox.forms import NetBoxModelForm from netbox.registry import registry from netbox.utils import get_data_backend_choices from utilities.forms import BootstrapMixin, get_field_value -from utilities.forms.fields import CommentField +from utilities.forms.fields import CommentField, JSONField from utilities.forms.widgets import HTMXSelect __all__ = ( @@ -132,6 +133,9 @@ class ConfigFormMetaclass(forms.models.ModelFormMetaclass): 'help_text': param.description, } field_kwargs.update(**param.field_kwargs) + if param.field is _JSONField: + # Replace with our own JSONField to get pretty JSON in config editor + param.field = JSONField param_fields[param.name] = param.field(**field_kwargs) attrs.update(param_fields) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 521df9c81..e07d17aca 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -612,7 +612,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer): required=False, allow_null=True ) - component = serializers.SerializerMethodField(read_only=True) + component = serializers.SerializerMethodField(read_only=True, allow_null=True) _depth = serializers.IntegerField(source='level', read_only=True) class Meta: @@ -685,7 +685,7 @@ class DeviceSerializer(NetBoxModelSerializer): ) status = ChoiceField(choices=DeviceStatusChoices, required=False) airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) - primary_ip = NestedIPAddressSerializer(read_only=True) + primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) oob_ip = NestedIPAddressSerializer(required=False, allow_null=True) @@ -735,7 +735,7 @@ class DeviceSerializer(NetBoxModelSerializer): class DeviceWithConfigContextSerializer(DeviceSerializer): - config_context = serializers.SerializerMethodField(read_only=True) + config_context = serializers.SerializerMethodField(read_only=True, allow_null=True) class Meta(DeviceSerializer.Meta): fields = [ @@ -1067,7 +1067,7 @@ class InventoryItemSerializer(NetBoxModelSerializer): required=False, allow_null=True ) - component = serializers.SerializerMethodField(read_only=True) + component = serializers.SerializerMethodField(read_only=True, allow_null=True) _depth = serializers.IntegerField(source='level', read_only=True) class Meta: diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 6773bc55f..cee8fcfba 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -976,21 +976,67 @@ class InventoryItemTemplateForm(ComponentTemplateForm): queryset=Manufacturer.objects.all(), required=False ) - component_type = ContentTypeChoiceField( - queryset=ContentType.objects.all(), - limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS, + # Assigned component selectors + consoleporttemplate = DynamicModelChoiceField( + queryset=ConsolePortTemplate.objects.all(), required=False, - widget=forms.HiddenInput + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Console port template') ) - component_id = forms.IntegerField( + consoleserverporttemplate = DynamicModelChoiceField( + queryset=ConsoleServerPortTemplate.objects.all(), required=False, - widget=forms.HiddenInput + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Console server port template') + ) + frontporttemplate = DynamicModelChoiceField( + queryset=FrontPortTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Front port template') + ) + interfacetemplate = DynamicModelChoiceField( + queryset=InterfaceTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Interface template') + ) + poweroutlettemplate = DynamicModelChoiceField( + queryset=PowerOutletTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Power outlet template') + ) + powerporttemplate = DynamicModelChoiceField( + queryset=PowerPortTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Power port template') + ) + rearporttemplate = DynamicModelChoiceField( + queryset=RearPortTemplate.objects.all(), + required=False, + query_params={ + 'device_type_id': '$device_type' + }, + label=_('Rear port template') ) fieldsets = ( (None, ( 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', - 'component_type', 'component_id', )), ) @@ -998,9 +1044,52 @@ class InventoryItemTemplateForm(ComponentTemplateForm): model = InventoryItemTemplate fields = [ 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', - 'component_type', 'component_id', ] + def __init__(self, *args, **kwargs): + instance = kwargs.get('instance') + initial = kwargs.get('initial', {}).copy() + component_type = initial.get('component_type') + component_id = initial.get('component_id') + + # Used for picking the default active tab for component selection + self.no_component = True + + if instance: + # When editing set the initial value for component selection + for component_model in ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS): + if type(instance.component) is component_model.model_class(): + initial[component_model.model] = instance.component + self.no_component = False + break + elif component_type and component_id: + # When adding the InventoryItem from a component page + if content_type := ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS).filter(pk=component_type).first(): + if component := content_type.model_class().objects.filter(pk=component_id).first(): + initial[content_type.model] = component + self.no_component = False + + kwargs['initial'] = initial + + super().__init__(*args, **kwargs) + + def clean(self): + super().clean() + + # Handle object assignment + selected_objects = [ + field for field in ( + 'consoleporttemplate', 'consoleserverporttemplate', 'frontporttemplate', 'interfacetemplate', + 'poweroutlettemplate', 'powerporttemplate', 'rearporttemplate' + ) if self.cleaned_data[field] + ] + if len(selected_objects) > 1: + raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component.")) + elif selected_objects: + self.instance.component = self.cleaned_data[selected_objects[0]] + else: + self.instance.component = None + # # Device components diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d0e92ff56..ce4bb5750 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1656,6 +1656,7 @@ class InventoryItemTemplateCreateView(generic.ComponentCreateView): queryset = InventoryItemTemplate.objects.all() form = forms.InventoryItemTemplateCreateForm model_form = forms.InventoryItemTemplateForm + template_name = 'dcim/inventoryitemtemplate_edit.html' def alter_object(self, instance, request): # Set component (if any) @@ -1673,6 +1674,7 @@ class InventoryItemTemplateCreateView(generic.ComponentCreateView): class InventoryItemTemplateEditView(generic.ObjectEditView): queryset = InventoryItemTemplate.objects.all() form = forms.InventoryItemTemplateForm + template_name = 'dcim/inventoryitemtemplate_edit.html' @register_model_view(InventoryItemTemplate, 'delete') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 8f9face41..70b7a78a4 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -265,6 +265,7 @@ class EventRuleForm(NetBoxModelForm): required=False, help_text=_('Enter parameters to pass to the action in JSON format.') ) + comments = CommentField() fieldsets = ( (_('Event Rule'), ('name', 'description', 'content_types', 'enabled', 'tags')), diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 33aa55a93..8dca73d94 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -262,7 +262,7 @@ class AvailableVLANSerializer(serializers.Serializer): Representation of a VLAN which does not exist in the database. """ vid = serializers.IntegerField(read_only=True) - group = NestedVLANGroupSerializer(read_only=True) + group = NestedVLANGroupSerializer(read_only=True, allow_null=True) def to_representation(self, instance): return { @@ -348,9 +348,9 @@ class AvailablePrefixSerializer(serializers.Serializer): """ Representation of a prefix which does not exist in the database. """ - family = serializers.IntegerField(read_only=True) + family = serializers.IntegerField(read_only=True, allow_null=True) prefix = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) + vrf = NestedVRFSerializer(read_only=True, allow_null=True) def to_representation(self, instance): if self.context.get('vrf'): @@ -429,9 +429,9 @@ class AvailableIPSerializer(serializers.Serializer): """ Representation of an IP address which does not exist in the database. """ - family = serializers.IntegerField(read_only=True) + family = serializers.IntegerField(read_only=True, allow_null=True) address = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) + vrf = NestedVRFSerializer(read_only=True, allow_null=True) description = serializers.CharField(required=False) def to_representation(self, instance): diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 25242d177..1a4155aab 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -73,11 +73,6 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): """ Base form for creating a NetBox objects from CSV data. Used for bulk importing. """ - id = forms.IntegerField( - label=_('Id'), - required=False, - help_text=_('Numeric ID of an existing object to update (if not creating a new object)') - ) tags = CSVModelMultipleChoiceField( label=_('Tags'), queryset=Tag.objects.all(), diff --git a/netbox/templates/dcim/inventoryitemtemplate_edit.html b/netbox/templates/dcim/inventoryitemtemplate_edit.html new file mode 100644 index 000000000..d3ac58e25 --- /dev/null +++ b/netbox/templates/dcim/inventoryitemtemplate_edit.html @@ -0,0 +1,104 @@ +{% extends 'generic/object_edit.html' %} +{% load static %} +{% load form_helpers %} +{% load helpers %} +{% load i18n %} + +{% block form %} +