From 46f734eba2da42d2dfefc230cd8c954e79dfaa40 Mon Sep 17 00:00:00 2001 From: "Jamie (Bear) Murphy" <1613241+ITJamie@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:57:14 +0100 Subject: [PATCH 001/160] fix error for is_oob_ip for non-device parents (#13621) * fix error for is_oob_ip for non-device parents * adjust oob_ip_id check to use hasattr --- netbox/ipam/models/ip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 553f5eb92..af0a0ef45 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -892,7 +892,7 @@ class IPAddress(PrimaryModel): def is_oob_ip(self): if self.assigned_object: parent = getattr(self.assigned_object, 'parent_object', None) - if parent.oob_ip_id == self.pk: + if hasattr(parent, "oob_ip_id") and parent.oob_ip_id == self.pk: return True return False From 316d991b33a110fdf102a0cdab0a2327737895d4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 08:14:57 -0400 Subject: [PATCH 002/160] Fixes #13630: Fix display of active status under user view --- docs/release-notes/version-3.6.md | 4 ++++ netbox/templates/users/user.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index d941fec34..7a8619176 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -2,6 +2,10 @@ ## v3.6.1 (FUTURE) +### Bug Fixes + +* [#13630](https://github.com/netbox-community/netbox/issues/13630) - Fix display of active status under user view + --- ## v3.6.0 (2023-08-30) diff --git a/netbox/templates/users/user.html b/netbox/templates/users/user.html index fe03f41ed..18c07c1cc 100644 --- a/netbox/templates/users/user.html +++ b/netbox/templates/users/user.html @@ -32,7 +32,7 @@ {% trans "Active" %} - {% checkmark object.active %} + {% checkmark object.is_active %} {% trans "Staff" %} From cb93abb0f486acdf6f7e9374b1ed6e6cb50365e4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 08:19:17 -0400 Subject: [PATCH 003/160] Fixes #13626: Correct filtering of recent activity list under user view --- docs/release-notes/version-3.6.md | 1 + netbox/users/views.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 7a8619176..16efa71bf 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#13626](https://github.com/netbox-community/netbox/issues/13626) - Correct filtering of recent activity list under user view * [#13630](https://github.com/netbox-community/netbox/issues/13630) - Fix display of active status under user view --- diff --git a/netbox/users/views.py b/netbox/users/views.py index 7ff9a8a4d..2e7a47c12 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -68,7 +68,7 @@ class UserView(generic.ObjectView): template_name = 'users/user.html' def get_extra_context(self, request, instance): - changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=request.user)[:20] + changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=instance)[:20] changelog_table = ObjectChangeTable(changelog) return { From 272d2c54d43b4f85fccb00c71986ce7719b88aee Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 31 Aug 2023 18:45:18 +0530 Subject: [PATCH 004/160] removes napalm references #13628 --- docs/models/dcim/platform.md | 14 ------------ netbox/dcim/api/serializers.py | 4 ---- netbox/netbox/config/parameters.py | 33 ----------------------------- netbox/templates/dcim/platform.html | 11 ---------- 4 files changed, 62 deletions(-) diff --git a/docs/models/dcim/platform.md b/docs/models/dcim/platform.md index dc332da74..0914d0aa6 100644 --- a/docs/models/dcim/platform.md +++ b/docs/models/dcim/platform.md @@ -23,17 +23,3 @@ If designated, this platform will be available for use only to devices assigned ### Configuration Template The default [configuration template](../extras/configtemplate.md) for devices assigned to this platform. - -### NAPALM Driver - -!!! warning "Deprecated Field" - NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6. - -The [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html) associated with this platform. - -### NAPALM Arguments - -!!! warning "Deprecated Field" - NAPALM integration was removed from NetBox core in v3.5 and is now available as a [plugin](https://github.com/netbox-community/netbox-napalm). This field will be removed in NetBox v3.6. - -Any additional arguments to send when invoking the NAPALM driver assigned to this platform. diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2f4eb6581..b43611dad 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -787,10 +787,6 @@ class ModuleSerializer(NetBoxModelSerializer): ] -class DeviceNAPALMSerializer(serializers.Serializer): - method = serializers.JSONField() - - # # Device components # diff --git a/netbox/netbox/config/parameters.py b/netbox/netbox/config/parameters.py index 8be5c97a9..31c4f693a 100644 --- a/netbox/netbox/config/parameters.py +++ b/netbox/netbox/config/parameters.py @@ -158,39 +158,6 @@ PARAMS = ( }, ), - # NAPALM - ConfigParam( - name='NAPALM_USERNAME', - label=_('NAPALM username'), - default='', - description=_("Username to use when connecting to devices via NAPALM") - ), - ConfigParam( - name='NAPALM_PASSWORD', - label=_('NAPALM password'), - default='', - description=_("Password to use when connecting to devices via NAPALM") - ), - ConfigParam( - name='NAPALM_TIMEOUT', - label=_('NAPALM timeout'), - default=30, - description=_("NAPALM connection timeout (in seconds)"), - field=forms.IntegerField - ), - ConfigParam( - name='NAPALM_ARGS', - label=_('NAPALM arguments'), - default={}, - description=_("Additional arguments to pass when invoking a NAPALM driver (as JSON data)"), - field=forms.JSONField, - field_kwargs={ - 'widget': forms.Textarea( - attrs={'class': 'vLargeTextField'} - ), - }, - ), - # User preferences ConfigParam( name='DEFAULT_USER_PREFERENCES', diff --git a/netbox/templates/dcim/platform.html b/netbox/templates/dcim/platform.html index a974f9f93..29f405b6e 100644 --- a/netbox/templates/dcim/platform.html +++ b/netbox/templates/dcim/platform.html @@ -44,17 +44,6 @@ {% trans "Config Template" %} {{ object.config_template|linkify|placeholder }} - - - {% trans "NAPALM Driver" %} - - - From 06f2c6f8673244bb35ba1d287c5a8200d8079954 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 08:08:10 -0400 Subject: [PATCH 005/160] Fixes #13632: Avoid raising exception when checking if FHRP group IP address is primary --- netbox/ipam/models/ip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index af0a0ef45..89977704a 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -892,7 +892,7 @@ class IPAddress(PrimaryModel): def is_oob_ip(self): if self.assigned_object: parent = getattr(self.assigned_object, 'parent_object', None) - if hasattr(parent, "oob_ip_id") and parent.oob_ip_id == self.pk: + if hasattr(parent, 'oob_ip') and parent.oob_ip_id == self.pk: return True return False @@ -900,9 +900,9 @@ class IPAddress(PrimaryModel): def is_primary_ip(self): if self.assigned_object: parent = getattr(self.assigned_object, 'parent_object', None) - if self.family == 4 and parent.primary_ip4_id == self.pk: + if self.family == 4 and hasattr(parent, 'primary_ip4') and parent.primary_ip4_id == self.pk: return True - if self.family == 6 and parent.primary_ip6_id == self.pk: + if self.family == 6 and hasattr(parent, 'primary_ip6') and parent.primary_ip6_id == self.pk: return True return False From 2544e2bf18a8639eda62a1d6d34cdd8ac2cbc09a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 08:52:46 -0400 Subject: [PATCH 006/160] Fixes #13622: Fix exception when viewing current config and no revisions have been created --- netbox/core/views.py | 8 +++++++- netbox/extras/models/models.py | 4 ++++ netbox/templates/extras/configrevision.html | 12 ++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/netbox/core/views.py b/netbox/core/views.py index c7c593770..e3c1a67aa 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -2,6 +2,7 @@ from django.contrib import messages from django.shortcuts import get_object_or_404, redirect from extras.models import ConfigRevision +from netbox.config import get_config from netbox.views import generic from netbox.views.generic.base import BaseObjectView from utilities.utils import count_related @@ -152,4 +153,9 @@ class ConfigView(generic.ObjectView): queryset = ConfigRevision.objects.all() def get_object(self, **kwargs): - return self.queryset.first() + if config := self.queryset.first(): + return config + # Instantiate a dummy default config if none has been created yet + return ConfigRevision( + data=get_config().defaults + ) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 91940d66e..90e8027b4 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -723,6 +723,8 @@ class ConfigRevision(models.Model): verbose_name_plural = _('config revisions') def __str__(self): + if not self.pk: + return gettext('Default configuration') if self.is_active: return gettext('Current configuration') return gettext('Config revision #{id}').format(id=self.pk) @@ -733,6 +735,8 @@ class ConfigRevision(models.Model): return super().__getattribute__(item) def get_absolute_url(self): + if not self.pk: + return reverse('core:config') # Default config view return reverse('extras:configrevision', args=[self.pk]) def activate(self): diff --git a/netbox/templates/extras/configrevision.html b/netbox/templates/extras/configrevision.html index 5937e842a..4f2abf30b 100644 --- a/netbox/templates/extras/configrevision.html +++ b/netbox/templates/extras/configrevision.html @@ -14,11 +14,11 @@
{% plugin_buttons object %} - {% if object.is_active and perms.extras.add_configrevision %} + {% if not object.pk or object.is_active and perms.extras.add_configrevision %} {% url 'extras:configrevision_add' as edit_url %} {% include "buttons/edit.html" with url=edit_url %} {% endif %} - {% if not object.is_active and perms.extras.delete_configrevision %} + {% if object.pk and not object.is_active and perms.extras.delete_configrevision %} {% delete_button object %} {% endif %}
@@ -28,6 +28,14 @@
{% endblock controls %} +{% block subtitle %} + {% if object.created %} +
+ {% trans "Created" %} {{ object.created|annotated_date }} +
+ {% endif %} +{% endblock subtitle %} + {% block content %}
From f962fb3b53a2fd3ca0110f51ea4a557b2313c306 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 11:23:44 -0400 Subject: [PATCH 007/160] Closes #13638: Add optional staff_only attribute to MenuItem (#13639) * Closes #13638: Add optional staff_only attribute to MenuItem * Add missing file * Add release note --- docs/plugins/development/navigation.md | 15 +++++++++------ netbox/extras/plugins/navigation.py | 3 ++- netbox/netbox/navigation/__init__.py | 1 + netbox/utilities/templatetags/navigation.py | 13 ++++++++----- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/plugins/development/navigation.md b/docs/plugins/development/navigation.md index 3e7762184..8d7580147 100644 --- a/docs/plugins/development/navigation.md +++ b/docs/plugins/development/navigation.md @@ -64,12 +64,15 @@ item1 = PluginMenuItem( A `PluginMenuItem` has the following attributes: -| Attribute | Required | Description | -|---------------|----------|------------------------------------------------------| -| `link` | Yes | Name of the URL path to which this menu item links | -| `link_text` | Yes | The text presented to the user | -| `permissions` | - | A list of permissions required to display this link | -| `buttons` | - | An iterable of PluginMenuButton instances to include | +| Attribute | Required | Description | +|---------------|----------|----------------------------------------------------------------------------------------------------------| +| `link` | Yes | Name of the URL path to which this menu item links | +| `link_text` | Yes | The text presented to the user | +| `permissions` | - | A list of permissions required to display this link | +| `staff_only` | - | Display only for users who have `is_staff` set to true (any specified permissions will also be required) | +| `buttons` | - | An iterable of PluginMenuButton instances to include | + +!!! info "The `staff_only` attribute was introduced in NetBox v3.6.1." ## Menu Buttons diff --git a/netbox/extras/plugins/navigation.py b/netbox/extras/plugins/navigation.py index 288a78512..2075c97b6 100644 --- a/netbox/extras/plugins/navigation.py +++ b/netbox/extras/plugins/navigation.py @@ -36,9 +36,10 @@ class PluginMenuItem: permissions = [] buttons = [] - def __init__(self, link, link_text, permissions=None, buttons=None): + def __init__(self, link, link_text, staff_only=False, permissions=None, buttons=None): self.link = link self.link_text = link_text + self.staff_only = staff_only if permissions is not None: if type(permissions) not in (list, tuple): raise TypeError("Permissions must be passed as a tuple or list.") diff --git a/netbox/netbox/navigation/__init__.py b/netbox/netbox/navigation/__init__.py index a05b1c495..4c7190bbb 100644 --- a/netbox/netbox/navigation/__init__.py +++ b/netbox/netbox/navigation/__init__.py @@ -34,6 +34,7 @@ class MenuItem: link: str link_text: str permissions: Optional[Sequence[str]] = () + staff_only: Optional[bool] = False buttons: Optional[Sequence[MenuItemButton]] = () diff --git a/netbox/utilities/templatetags/navigation.py b/netbox/utilities/templatetags/navigation.py index 4a229e952..7534d7034 100644 --- a/netbox/utilities/templatetags/navigation.py +++ b/netbox/utilities/templatetags/navigation.py @@ -26,11 +26,14 @@ def nav(context: Context) -> Dict: for group in menu.groups: items = [] for item in group.items: - if user.has_perms(item.permissions): - buttons = [ - button for button in item.buttons if user.has_perms(button.permissions) - ] - items.append((item, buttons)) + if not user.has_perms(item.permissions): + continue + if item.staff_only and not user.is_staff: + continue + buttons = [ + button for button in item.buttons if user.has_perms(button.permissions) + ] + items.append((item, buttons)) if items: groups.append((group, items)) if groups: From 78966e12a919ff149c8f338e0c708b44f2db182f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 12:02:59 -0400 Subject: [PATCH 008/160] Fixes #13620: Show admin menu items only for staff users --- netbox/netbox/navigation/menu.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 5c7502a03..5b64cfc1e 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -360,6 +360,7 @@ ADMIN_MENU = Menu( link=f'users:netboxuser_list', link_text=_('Users'), permissions=[f'auth.view_user'], + staff_only=True, buttons=( MenuItemButton( link=f'users:netboxuser_add', @@ -382,6 +383,7 @@ ADMIN_MENU = Menu( link=f'users:netboxgroup_list', link_text=_('Groups'), permissions=[f'auth.view_group'], + staff_only=True, buttons=( MenuItemButton( link=f'users:netboxgroup_add', @@ -399,8 +401,20 @@ ADMIN_MENU = Menu( ) ) ), - get_model_item('users', 'token', _('API Tokens')), - get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']), + MenuItem( + link=f'users:token_list', + link_text=_('API Tokens'), + permissions=[f'users.view_token'], + staff_only=True, + buttons=get_model_buttons('users', 'token') + ), + MenuItem( + link=f'users:objectpermission_list', + link_text=_('Permissions'), + permissions=[f'users.view_objectpermission'], + staff_only=True, + buttons=get_model_buttons('users', 'objectpermission', actions=['add']) + ), ), ), MenuGroup( @@ -409,12 +423,14 @@ ADMIN_MENU = Menu( MenuItem( link='core:config', link_text=_('Current Config'), - permissions=['extras.view_configrevision'] + permissions=['extras.view_configrevision'], + staff_only=True ), MenuItem( link='extras:configrevision_list', link_text=_('Config Revisions'), - permissions=['extras.view_configrevision'] + permissions=['extras.view_configrevision'], + staff_only=True ), ), ), From 2503568875d3c7ae91959d2ff7817c769b2e6d9c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 12:23:59 -0400 Subject: [PATCH 009/160] Changelog for #13619, #13620, #13622, #13628, #13632, #13638 --- docs/release-notes/version-3.6.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 16efa71bf..3623fe900 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -2,10 +2,19 @@ ## v3.6.1 (FUTURE) +### Enhancements + +* [#13638](https://github.com/netbox-community/netbox/issues/13638) - Add optional `staff_only` attribute to MenuItem + ### Bug Fixes +* [#13619](https://github.com/netbox-community/netbox/issues/13619) - Fix exception when viewing IP address assigned to a virtual machine +* [#13620](https://github.com/netbox-community/netbox/issues/13620) - Show admin menu items only for staff users +* [#13622](https://github.com/netbox-community/netbox/issues/13622) - Fix exception when viewing current config and no revisions have been created * [#13626](https://github.com/netbox-community/netbox/issues/13626) - Correct filtering of recent activity list under user view +* [#13628](https://github.com/netbox-community/netbox/issues/13628) - Remove stale references to obsolete NAPALM integration * [#13630](https://github.com/netbox-community/netbox/issues/13630) - Fix display of active status under user view +* [#13632](https://github.com/netbox-community/netbox/issues/13632) - Avoid raising exception when checking if FHRP group IP address is primary --- From 0cdc26e0130cb56605018778b64f2ddd3ff1d406 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 13:05:04 -0400 Subject: [PATCH 010/160] Fixes #13642: Move migration logic overrides from individual mgmt commands to core --- netbox/core/apps.py | 11 +++++++++++ netbox/core/management/commands/makemigrations.py | 12 ------------ netbox/core/management/commands/migrate.py | 7 ------- 3 files changed, 11 insertions(+), 19 deletions(-) delete mode 100644 netbox/core/management/commands/migrate.py diff --git a/netbox/core/apps.py b/netbox/core/apps.py index ffcf0b4ea..2d999c57e 100644 --- a/netbox/core/apps.py +++ b/netbox/core/apps.py @@ -1,4 +1,15 @@ from django.apps import AppConfig +from django.db import models +from django.db.migrations.operations import AlterModelOptions + +from utilities.migration import custom_deconstruct + +# Ignore verbose_name & verbose_name_plural Meta options when calculating model migrations +AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name') +AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name_plural') + +# Use our custom destructor to ignore certain attributes when calculating field migrations +models.Field.deconstruct = custom_deconstruct class CoreConfig(AppConfig): diff --git a/netbox/core/management/commands/makemigrations.py b/netbox/core/management/commands/makemigrations.py index 10874418a..ce40bd3cc 100644 --- a/netbox/core/management/commands/makemigrations.py +++ b/netbox/core/management/commands/makemigrations.py @@ -1,18 +1,6 @@ -# noinspection PyUnresolvedReferences from django.conf import settings from django.core.management.base import CommandError from django.core.management.commands.makemigrations import Command as _Command -from django.db import models -from django.db.migrations.operations import AlterModelOptions - -from utilities.migration import custom_deconstruct - -# Monkey patch AlterModelOptions to ignore verbose name attributes -AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name') -AlterModelOptions.ALTER_OPTION_KEYS.remove('verbose_name_plural') - -# Set our custom deconstructor for fields -models.Field.deconstruct = custom_deconstruct class Command(_Command): diff --git a/netbox/core/management/commands/migrate.py b/netbox/core/management/commands/migrate.py deleted file mode 100644 index 8d5e45a40..000000000 --- a/netbox/core/management/commands/migrate.py +++ /dev/null @@ -1,7 +0,0 @@ -# noinspection PyUnresolvedReferences -from django.core.management.commands.migrate import Command -from django.db import models - -from utilities.migration import custom_deconstruct - -models.Field.deconstruct = custom_deconstruct From 679cc8fdda86b38c99791c9c676b6ca05ef6e697 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Aug 2023 13:50:54 -0400 Subject: [PATCH 011/160] Fixes #13596: Always display "render config" tab for devices & VMs --- netbox/dcim/views.py | 1 - netbox/virtualization/views.py | 1 - 2 files changed, 2 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4377e9ee8..25b44318b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2033,7 +2033,6 @@ class DeviceRenderConfigView(generic.ObjectView): template_name = 'dcim/device/render_config.html' tab = ViewTab( label=_('Render Config'), - permission='extras.view_configtemplate', weight=2100 ) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index cbe953040..173d7047b 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -397,7 +397,6 @@ class VirtualMachineRenderConfigView(generic.ObjectView): template_name = 'virtualization/virtualmachine/render_config.html' tab = ViewTab( label=_('Render Config'), - permission='extras.view_configtemplate', weight=2100 ) From 296166da95d9c8bcc0c1139215c6c2d64a0c317d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Sep 2023 10:07:11 -0400 Subject: [PATCH 012/160] Fixes #13656: Correct decoding of BinaryField content for Django 4.2 --- netbox/core/models/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index 8e372c2eb..54a43c7ef 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -316,7 +316,7 @@ class DataFile(models.Model): if not self.data: return None try: - return bytes(self.data, 'utf-8') + return self.data.decode('utf-8') except UnicodeDecodeError: return None From 7848beedceb5b5e69d651c4a400a510fa83f18ec Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 31 Aug 2023 01:18:18 +0530 Subject: [PATCH 013/160] adds additional parameters for token provision api #12870 --- netbox/users/api/serializers.py | 42 +++++++++++++++++++++++++++++---- netbox/users/api/views.py | 34 +++++++++----------------- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 1f4bf4ea0..75ab877cf 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,11 +1,12 @@ from django.conf import settings +from django.contrib.auth import authenticate 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 from rest_framework import serializers -from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import AuthenticationFailed, PermissionDenied from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer @@ -107,9 +108,42 @@ class TokenSerializer(ValidatedModelSerializer): return super().validate(data) -class TokenProvisionSerializer(serializers.Serializer): - username = serializers.CharField() - password = serializers.CharField() +class TokenProvisionSerializer(TokenSerializer): + user = NestedUserSerializer( + read_only=True + ) + username = serializers.CharField( + write_only=True + ) + password = serializers.CharField( + write_only=True + ) + last_used = serializers.DateTimeField( + read_only=True + ) + key = serializers.CharField( + read_only=True + ) + + class Meta: + model = Token + fields = ( + 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', + 'allowed_ips', 'username', 'password', + ) + + def validate(self, data): + # Validate the username and password + username = data.pop('username') + password = data.pop('password') + user = authenticate(request=self.context.get('request'), username=username, password=password) + if user is None: + raise AuthenticationFailed("Invalid username/password") + + # Inject the user into the validated data + data['user'] = user + + return data class ObjectPermissionSerializer(ValidatedModelSerializer): diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index 9cf5b1ac5..62a32c71b 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -1,3 +1,4 @@ +import logging from django.contrib.auth import authenticate from django.contrib.auth import get_user_model from django.contrib.auth.models import Group @@ -63,34 +64,21 @@ class TokenProvisionView(APIView): @extend_schema( request=serializers.TokenProvisionSerializer, responses={ - 201: serializers.TokenSerializer, + 201: serializers.TokenProvisionSerializer, 401: OpenApiTypes.OBJECT, } ) def post(self, request): - serializer = serializers.TokenProvisionSerializer(data=request.data) - serializer.is_valid() + serializer = serializers.TokenProvisionSerializer(data=request.data, context={'request': request}) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return Response(serializer.data, status=HTTP_201_CREATED) - # Authenticate the user account based on the provided credentials - username = serializer.data.get('username') - password = serializer.data.get('password') - if not username or not password: - raise AuthenticationFailed("Username and password must be provided to provision a token.") - user = authenticate(request=request, username=username, password=password) - if user is None: - raise AuthenticationFailed("Invalid username/password") - - # Create a new Token for the User - token = Token(user=user) - token.save() - data = serializers.TokenSerializer(token, context={'request': request}).data - # Manually append the token key, which is normally write-only - data['key'] = token.key - - return Response(data, status=HTTP_201_CREATED) - - def get_serializer_class(self): - return serializers.TokenSerializer + def perform_create(self, serializer): + model = serializer.Meta.model + logger = logging.getLogger(f'netbox.api.views.TokenProvisionView') + logger.info(f"Creating new {model._meta.verbose_name}") + serializer.save() # From c38884fa11cf3e7c5fc365001f099d732f9456f5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Sep 2023 11:43:34 -0400 Subject: [PATCH 014/160] Add description & expires fields to token test --- netbox/users/tests/test_api.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 859dd0b83..001142410 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -141,17 +141,25 @@ class TokenTest( """ Test the provisioning of a new REST API token given a valid username and password. """ - data = { + user_credentials = { 'username': 'user1', 'password': 'abc123', } - user = User.objects.create_user(**data) + user = User.objects.create_user(**user_credentials) + + data = { + **user_credentials, + 'description': 'My API token', + 'expires': '2099-12-31T23:59:59Z', + } url = reverse('users-api:token_provision') response = self.client.post(url, data, format='json', **self.header) self.assertEqual(response.status_code, 201) self.assertIn('key', response.data) self.assertEqual(len(response.data['key']), 40) + self.assertEqual(response.data['description'], data['description']) + self.assertEqual(response.data['expires'], data['expires']) token = Token.objects.get(user=user) self.assertEqual(token.key, response.data['key']) From 559f65f6b208ab0f361c10a7ab39ef4a341f7747 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Sep 2023 13:22:07 -0400 Subject: [PATCH 015/160] Add #12906 to v3.6.0 changelog --- docs/release-notes/version-3.6.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 3623fe900..78757e996 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -23,6 +23,7 @@ ### Breaking Changes * PostgreSQL 11 is no longer supported (dropped in Django 4.2). NetBox v3.6 requires PostgreSQL 12 or later. +* The `boto3` and `dulwich` packages are no longer installed automatically. If needed for S3/git remote data backend support, add them to `local_requirements.txt` to ensure their installation. * The `device_role` field on the Device model has been renamed to `role`. The `device_role` field has been temporarily retained on the REST API serializer for devices for backward compatibility, but is read-only. * The `choices` array field has been removed from the CustomField model. Any defined choices are automatically migrated to CustomFieldChoiceSets, accessible via the new `choice_set` field on the CustomField model. * The `napalm_driver` and `napalm_args` fields (which were deprecated in v3.5) have been removed from the Platform model. @@ -103,8 +104,9 @@ Tags may now be restricted to use with designated object types. Tags that have n * [#11766](https://github.com/netbox-community/netbox/issues/11766) - Remove obsolete custom `ChoiceField` and `MultipleChoiceField` classes * [#12180](https://github.com/netbox-community/netbox/issues/12180) - All API endpoints for available objects (e.g. IP addresses) now inherit from a common parent view * [#12237](https://github.com/netbox-community/netbox/issues/12237) - Upgrade Django to v4.2 -* [#12794](https://github.com/netbox-community/netbox/issues/12794) - Avoid direct imports of Django's stock user model * [#12320](https://github.com/netbox-community/netbox/issues/12320) - Remove obsolete fields `napalm_driver` and `napalm_args` from Platform +* [#12794](https://github.com/netbox-community/netbox/issues/12794) - Avoid direct imports of Django's stock user model +* [#12906](https://github.com/netbox-community/netbox/issues/12906) - The `boto3` (AWS) and `dulwich` (git) packages for remote data sources are now optional requirements * [#12964](https://github.com/netbox-community/netbox/issues/12964) - Drop support for PostgreSQL 11 * [#13309](https://github.com/netbox-community/netbox/issues/13309) - User account-specific resources have been moved to a new `account` app for better organization From 004daca862a37243f90fd14b02bcc9ec88e8c6c1 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 2 Sep 2023 02:28:31 +0530 Subject: [PATCH 016/160] Adds rename button on the list page for device components (#13564) * adds interface rename button on the list page #13444 * adds rename view on all device components #13564 * Condense component views to a single template --------- Co-authored-by: Jeremy Stretch --- netbox/dcim/views.py | 90 +++++++++++++++++++++++ netbox/templates/dcim/component_list.html | 22 ++++++ 2 files changed, 112 insertions(+) create mode 100644 netbox/templates/dcim/component_list.html diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 25b44318b..2f661e613 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2184,6 +2184,15 @@ class ConsolePortListView(generic.ObjectListView): filterset = filtersets.ConsolePortFilterSet filterset_form = forms.ConsolePortFilterForm table = tables.ConsolePortTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(ConsolePort) @@ -2247,6 +2256,15 @@ class ConsoleServerPortListView(generic.ObjectListView): filterset = filtersets.ConsoleServerPortFilterSet filterset_form = forms.ConsoleServerPortFilterForm table = tables.ConsoleServerPortTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(ConsoleServerPort) @@ -2310,6 +2328,15 @@ class PowerPortListView(generic.ObjectListView): filterset = filtersets.PowerPortFilterSet filterset_form = forms.PowerPortFilterForm table = tables.PowerPortTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(PowerPort) @@ -2373,6 +2400,15 @@ class PowerOutletListView(generic.ObjectListView): filterset = filtersets.PowerOutletFilterSet filterset_form = forms.PowerOutletFilterForm table = tables.PowerOutletTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(PowerOutlet) @@ -2436,6 +2472,15 @@ class InterfaceListView(generic.ObjectListView): filterset = filtersets.InterfaceFilterSet filterset_form = forms.InterfaceFilterForm table = tables.InterfaceTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(Interface) @@ -2547,6 +2592,15 @@ class FrontPortListView(generic.ObjectListView): filterset = filtersets.FrontPortFilterSet filterset_form = forms.FrontPortFilterForm table = tables.FrontPortTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(FrontPort) @@ -2610,6 +2664,15 @@ class RearPortListView(generic.ObjectListView): filterset = filtersets.RearPortFilterSet filterset_form = forms.RearPortFilterForm table = tables.RearPortTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(RearPort) @@ -2673,6 +2736,15 @@ class ModuleBayListView(generic.ObjectListView): filterset = filtersets.ModuleBayFilterSet filterset_form = forms.ModuleBayFilterForm table = tables.ModuleBayTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(ModuleBay) @@ -2728,6 +2800,15 @@ class DeviceBayListView(generic.ObjectListView): filterset = filtersets.DeviceBayFilterSet filterset_form = forms.DeviceBayFilterForm table = tables.DeviceBayTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(DeviceBay) @@ -2852,6 +2933,15 @@ class InventoryItemListView(generic.ObjectListView): filterset = filtersets.InventoryItemFilterSet filterset_form = forms.InventoryItemFilterForm table = tables.InventoryItemTable + template_name = 'dcim/component_list.html' + actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename') + action_perms = defaultdict(set, **{ + 'add': {'add'}, + 'import': {'add'}, + 'bulk_edit': {'change'}, + 'bulk_delete': {'delete'}, + 'bulk_rename': {'change'}, + }) @register_model_view(InventoryItem) diff --git a/netbox/templates/dcim/component_list.html b/netbox/templates/dcim/component_list.html new file mode 100644 index 000000000..a80dcfea8 --- /dev/null +++ b/netbox/templates/dcim/component_list.html @@ -0,0 +1,22 @@ +{% extends 'generic/object_list.html' %} +{% load buttons %} +{% load helpers %} +{% load i18n %} + +{% block bulk_buttons %} +
+ {% if 'bulk_edit' in actions %} + {% bulk_edit_button model query_params=request.GET %} + {% endif %} + {% if 'bulk_rename' in actions %} + {% with bulk_rename_view=model|validated_viewname:"bulk_rename" %} + + {% endwith %} + {% endif %} +
+ {% if 'bulk_delete' in actions %} + {% bulk_delete_button model query_params=request.GET %} + {% endif %} +{% endblock %} From 6db661689218dfa4b181ffccfba30e0b22609892 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Sep 2023 17:14:59 -0400 Subject: [PATCH 017/160] Changelog for #12870, #13444, #13596, #13642, #13657 --- docs/release-notes/version-3.6.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 78757e996..cb11236a1 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -4,17 +4,22 @@ ### Enhancements +* [#12870](https://github.com/netbox-community/netbox/issues/12870) - Support setting token expiration time using the provisioning API endpoint +* [#13444](https://github.com/netbox-community/netbox/issues/13444) - Add bulk rename functionality to the global device component lists * [#13638](https://github.com/netbox-community/netbox/issues/13638) - Add optional `staff_only` attribute to MenuItem ### Bug Fixes * [#13619](https://github.com/netbox-community/netbox/issues/13619) - Fix exception when viewing IP address assigned to a virtual machine +* [#13596](https://github.com/netbox-community/netbox/issues/13596) - Always display "render config" tab for devices and virtual machines * [#13620](https://github.com/netbox-community/netbox/issues/13620) - Show admin menu items only for staff users * [#13622](https://github.com/netbox-community/netbox/issues/13622) - Fix exception when viewing current config and no revisions have been created * [#13626](https://github.com/netbox-community/netbox/issues/13626) - Correct filtering of recent activity list under user view * [#13628](https://github.com/netbox-community/netbox/issues/13628) - Remove stale references to obsolete NAPALM integration * [#13630](https://github.com/netbox-community/netbox/issues/13630) - Fix display of active status under user view * [#13632](https://github.com/netbox-community/netbox/issues/13632) - Avoid raising exception when checking if FHRP group IP address is primary +* [#13642](https://github.com/netbox-community/netbox/issues/13642) - Suppress warning about unreflected model changes when applying migrations +* [#13657](https://github.com/netbox-community/netbox/issues/13657) - Fix decoding of data file content --- From 9be5918c83f778915258d83497755cc814f2e4ec Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Sep 2023 11:16:42 -0400 Subject: [PATCH 018/160] Fixes #13684: Enable modying the configuration when maintenance mode is enabled --- netbox/netbox/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4ad783161..2f00db758 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -496,6 +496,7 @@ AUTH_EXEMPT_PATHS = ( # All URLs starting with a string listed here are exempt from maintenance mode enforcement MAINTENANCE_EXEMPT_PATHS = ( f'/{BASE_PATH}admin/', + f'/{BASE_PATH}extras/config-revisions/', # Allow modifying the configuration ) SERIALIZATION_MODULES = { From 9d851924c8909b6945f41ce63b4327f9488e3aaf Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 6 Sep 2023 05:44:25 -0700 Subject: [PATCH 019/160] 13674 fix ReportSerializer (#13688) * 13674 fix ReportSerializer * Remove test_methods attr from Report class --------- Co-authored-by: Jeremy Stretch --- netbox/extras/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 4da5fa629..e007db43d 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -479,7 +479,7 @@ class ReportSerializer(serializers.Serializer): module = serializers.CharField(max_length=255) name = serializers.CharField(max_length=255) description = serializers.CharField(max_length=255, required=False) - test_methods = serializers.ListField(child=serializers.CharField(max_length=255)) + test_methods = serializers.ListField(child=serializers.CharField(max_length=255), read_only=True) result = NestedJobSerializer() display = serializers.SerializerMethodField(read_only=True) From 2d1457b94bf6e4e7c75c03c6dd45901b53857e77 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 6 Sep 2023 09:47:18 -0500 Subject: [PATCH 020/160] Fixes: #13682 - Fix custom field exceptions and validation (#13685) * Fixes: #13682 - Fix custom field exceptions and validation * Add tests * Remove default setting for multi-select/multi-object and return slice of choices and annotate. * Remove redundant default choice valiadtion; introduce values property on CustomFieldChoiceSet * Refactor test --------- Co-authored-by: Jeremy Stretch --- netbox/extras/models/customfields.py | 32 +++++---- netbox/extras/tests/test_customfields.py | 91 ++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 15 deletions(-) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index ac68855a0..0c4a0c615 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -282,7 +282,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): raise ValidationError({ 'default': _( 'Invalid default value "{default}": {message}' - ).format(default=self.default, message=self.message) + ).format(default=self.default, message=err.message) }) # Minimum/maximum values can be set only for numeric fields @@ -317,14 +317,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): 'choice_set': _("Choices may be set only on selection fields.") }) - # A selection field's default (if any) must be present in its available choices - if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices: - raise ValidationError({ - 'default': _( - "The specified default value ({default}) is not listed as an available choice." - ).format(default=self.default) - }) - # Object fields must define an object_type; other fields must not if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): if not self.object_type: @@ -650,19 +642,22 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): # Validate selected choice elif self.type == CustomFieldTypeChoices.TYPE_SELECT: - if value not in [c[0] for c in self.choices]: + if value not in self.choice_set.values: raise ValidationError( - _("Invalid choice ({value}). Available choices are: {choices}").format( - value=value, choices=', '.join(self.choices) + _("Invalid choice ({value}) for choice set {choiceset}.").format( + value=value, + choiceset=self.choice_set ) ) # Validate all selected choices elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT: - if not set(value).issubset([c[0] for c in self.choices]): + if not set(value).issubset(self.choice_set.values): raise ValidationError( - _("Invalid choice(s) ({invalid_choices}). Available choices are: {available_choices}").format( - invalid_choices=', '.join(value), available_choices=', '.join(self.choices)) + _("Invalid choice(s) ({value}) for choice set {choiceset}.").format( + value=value, + choiceset=self.choice_set + ) ) # Validate selected object @@ -747,6 +742,13 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel def choices_count(self): return len(self.choices) + @property + def values(self): + """ + Returns an iterator of the valid choice values. + """ + return (x[0] for x in self.choices) + def clean(self): if not self.base_choices and not self.extra_choices: raise ValidationError(_("Must define base or extra choices.")) diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 019aef235..a8153e1bb 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -427,6 +427,97 @@ class CustomFieldTest(TestCase): self.assertNotIn('field1', site.custom_field_data) self.assertEqual(site.custom_field_data['field2'], FIELD_DATA) + def test_default_value_validation(self): + choiceset = CustomFieldChoiceSet.objects.create( + name="Test Choice Set", + extra_choices=( + ('choice1', 'Choice 1'), + ('choice2', 'Choice 2'), + ) + ) + site = Site.objects.create(name='Site 1', slug='site-1') + object_type = ContentType.objects.get_for_model(Site) + + # Text + CustomField(name='test', type='text', required=True, default="Default text").full_clean() + + # Integer + CustomField(name='test', type='integer', required=True, default=1).full_clean() + with self.assertRaises(ValidationError): + CustomField(name='test', type='integer', required=True, default='xxx').full_clean() + + # Boolean + CustomField(name='test', type='boolean', required=True, default=True).full_clean() + with self.assertRaises(ValidationError): + CustomField(name='test', type='boolean', required=True, default='xxx').full_clean() + + # Date + CustomField(name='test', type='date', required=True, default="2023-02-25").full_clean() + with self.assertRaises(ValidationError): + CustomField(name='test', type='date', required=True, default='xxx').full_clean() + + # Datetime + CustomField(name='test', type='datetime', required=True, default="2023-02-25 02:02:02").full_clean() + with self.assertRaises(ValidationError): + CustomField(name='test', type='datetime', required=True, default='xxx').full_clean() + + # URL + CustomField(name='test', type='url', required=True, default="https://www.netbox.dev").full_clean() + + # JSON + CustomField(name='test', type='json', required=True, default='{"test": "object"}').full_clean() + + # Selection + CustomField(name='test', type='select', required=True, choice_set=choiceset, default='choice1').full_clean() + with self.assertRaises(ValidationError): + CustomField(name='test', type='select', required=True, choice_set=choiceset, default='xxx').full_clean() + + # Multi-select + CustomField( + name='test', + type='multiselect', + required=True, + choice_set=choiceset, + default=['choice1'] # Single default choice + ).full_clean() + CustomField( + name='test', + type='multiselect', + required=True, + choice_set=choiceset, + default=['choice1', 'choice2'] # Multiple default choices + ).full_clean() + with self.assertRaises(ValidationError): + CustomField( + name='test', + type='multiselect', + required=True, + choice_set=choiceset, + default=['xxx'] + ).full_clean() + + # Object + CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean() + with self.assertRaises(ValidationError): + CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean() + + # Multi-object + CustomField( + name='test', + type='multiobject', + required=True, + object_type=object_type, + default=[site.pk] + ).full_clean() + with self.assertRaises(ValidationError): + CustomField( + name='test', + type='multiobject', + required=True, + object_type=object_type, + default=["xxx"] + ).full_clean() + class CustomFieldManagerTest(TestCase): From bb6b4d01c1f501e379fc767fa8b6327c927648a5 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 6 Sep 2023 07:49:40 -0700 Subject: [PATCH 021/160] 12553 prefix serializer to IPAddress (#13592) * 12553 prefix serializer to IPAddress * Introduce IPNetworkField to handle prefix serialization --------- Co-authored-by: Jeremy Stretch --- netbox/ipam/api/field_serializers.py | 37 ++++++++++++++++++++-------- netbox/ipam/api/serializers.py | 9 +++---- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/netbox/ipam/api/field_serializers.py b/netbox/ipam/api/field_serializers.py index d44d8b7d4..d12530a60 100644 --- a/netbox/ipam/api/field_serializers.py +++ b/netbox/ipam/api/field_serializers.py @@ -1,21 +1,18 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from ipam import models from netaddr import AddrFormatError, IPNetwork -__all__ = [ +__all__ = ( 'IPAddressField', -] + 'IPNetworkField', +) -# -# IP address field -# - class IPAddressField(serializers.CharField): - """IPAddressField with mask""" - + """ + An IPv4 or IPv6 address with optional mask + """ default_error_messages = { 'invalid': _('Enter a valid IPv4 or IPv6 address with optional mask.'), } @@ -24,7 +21,27 @@ class IPAddressField(serializers.CharField): try: return IPNetwork(data) except AddrFormatError: - raise serializers.ValidationError("Invalid IP address format: {}".format(data)) + raise serializers.ValidationError(_("Invalid IP address format: {data}").format(data)) + except (TypeError, ValueError) as e: + raise serializers.ValidationError(e) + + def to_representation(self, value): + return str(value) + + +class IPNetworkField(serializers.CharField): + """ + An IPv4 or IPv6 prefix + """ + default_error_messages = { + 'invalid': _('Enter a valid IPv4 or IPv6 prefix and mask in CIDR notation.'), + } + + def to_internal_value(self, data): + try: + return IPNetwork(data) + except AddrFormatError: + raise serializers.ValidationError(_("Invalid IP prefix format: {data}").format(data)) except (TypeError, ValueError) as e: raise serializers.ValidationError(e) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index c2cf38fe7..6882de56d 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -13,7 +13,7 @@ from tenancy.api.nested_serializers import NestedTenantSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import NestedVirtualMachineSerializer from .nested_serializers import * -from .field_serializers import IPAddressField +from .field_serializers import IPAddressField, IPNetworkField # @@ -138,7 +138,7 @@ class AggregateSerializer(NetBoxModelSerializer): family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) rir = NestedRIRSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True) - prefix = serializers.CharField() + prefix = IPNetworkField() class Meta: model = Aggregate @@ -146,7 +146,6 @@ class AggregateSerializer(NetBoxModelSerializer): 'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] - read_only_fields = ['family'] # @@ -306,7 +305,7 @@ class PrefixSerializer(NetBoxModelSerializer): role = NestedRoleSerializer(required=False, allow_null=True) children = serializers.IntegerField(read_only=True) _depth = serializers.IntegerField(read_only=True) - prefix = serializers.CharField() + prefix = IPNetworkField() class Meta: model = Prefix @@ -315,7 +314,6 @@ class PrefixSerializer(NetBoxModelSerializer): 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children', '_depth', ] - read_only_fields = ['family'] class PrefixLengthSerializer(serializers.Serializer): @@ -386,7 +384,6 @@ class IPRangeSerializer(NetBoxModelSerializer): 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] - read_only_fields = ['family'] # From 90ab4b3c8671913af8a0e15448eabc44c9e5bcaf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 Sep 2023 14:04:57 -0400 Subject: [PATCH 022/160] Release v3.6.1 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.6.md | 6 +++++- netbox/netbox/settings.py | 2 +- requirements.txt | 12 ++++++------ 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 744770180..ec7d667e6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.6.0 + placeholder: v3.6.1 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 5cf9b72ab..dc27ebd26 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.6.0 + placeholder: v3.6.1 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index cb11236a1..e9e958a9f 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -1,6 +1,6 @@ # NetBox v3.6 -## v3.6.1 (FUTURE) +## v3.6.1 (2023-09-06) ### Enhancements @@ -10,6 +10,7 @@ ### Bug Fixes +* [#12553](https://github.com/netbox-community/netbox/issues/12552) - Ensure `family` attribute is always returned when creating aggregates and prefixes via REST API * [#13619](https://github.com/netbox-community/netbox/issues/13619) - Fix exception when viewing IP address assigned to a virtual machine * [#13596](https://github.com/netbox-community/netbox/issues/13596) - Always display "render config" tab for devices and virtual machines * [#13620](https://github.com/netbox-community/netbox/issues/13620) - Show admin menu items only for staff users @@ -20,6 +21,9 @@ * [#13632](https://github.com/netbox-community/netbox/issues/13632) - Avoid raising exception when checking if FHRP group IP address is primary * [#13642](https://github.com/netbox-community/netbox/issues/13642) - Suppress warning about unreflected model changes when applying migrations * [#13657](https://github.com/netbox-community/netbox/issues/13657) - Fix decoding of data file content +* [#13674](https://github.com/netbox-community/netbox/issues/13674) - Fix retrieving individual report via REST API +* [#13682](https://github.com/netbox-community/netbox/issues/13682) - Fix error message returned when validation of custom field default value fails +* [#13684](https://github.com/netbox-community/netbox/issues/13684) - Enable modying the configuration when maintenance mode is enabled --- diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 2f00db758..75099a029 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.6.1-dev' +VERSION = '3.6.1' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index b313f98d6..54f1334ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bleach==6.0.0 -Django==4.2.4 +Django==4.2.5 django-cors-headers==4.2.0 django-debug-toolbar==4.2.0 django-filter==23.2 @@ -12,23 +12,23 @@ django-rich==1.7.0 django-rq==2.8.1 django-tables2==2.6.0 django-taggit==4.0.0 -django-timezone-field==5.1 +django-timezone-field==6.0 djangorestframework==3.14.0 drf-spectacular==0.26.4 -drf-spectacular-sidecar==2023.8.1 +drf-spectacular-sidecar==2023.9.1 feedparser==6.0.10 graphene-django==3.0.0 gunicorn==21.2.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.2.5 -mkdocstrings[python-legacy]==0.22.0 +mkdocs-material==9.2.7 +mkdocstrings[python-legacy]==0.23.0 netaddr==0.8.0 Pillow==10.0.0 psycopg[binary,pool]==3.1.10 PyYAML==6.0.1 sentry-sdk==1.30.0 -social-auth-app-django==5.2.0 +social-auth-app-django==5.3.0 social-auth-core[openidconnect]==4.4.2 svgwrite==1.4.3 tablib==3.5.0 From a8a36c0a8fa39250792abeb493ed3065bf0d9fdc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 6 Sep 2023 14:26:19 -0400 Subject: [PATCH 023/160] PRVB --- docs/release-notes/version-3.6.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index e9e958a9f..d1cb68532 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -1,5 +1,9 @@ # NetBox v3.6 +## v3.6.2 (FUTURE) + +--- + ## v3.6.1 (2023-09-06) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 75099a029..c22281e9e 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.6.1' +VERSION = '3.6.2-dev' # Hostname HOSTNAME = platform.node() From b5125e512fc8a13281381e6822d894a07f94eb11 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Sep 2023 13:52:19 -0400 Subject: [PATCH 024/160] Fixes #13721: Filter VLAN choices by selected site (if any) when creating a prefix --- docs/release-notes/version-3.6.md | 4 ++++ netbox/ipam/forms/model_forms.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index d1cb68532..e0b13f4ae 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -2,6 +2,10 @@ ## v3.6.2 (FUTURE) +### Bug Fixes + +* [#13721](https://github.com/netbox-community/netbox/issues/13721) - Filter VLAN choices by selected site (if any) when creating a prefix + --- ## v3.6.1 (2023-09-06) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index c466e279f..cc147cec4 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -215,6 +215,9 @@ class PrefixForm(TenancyForm, NetBoxModelForm): queryset=VLAN.objects.all(), required=False, selector=True, + query_params={ + 'site_id': '$site', + }, label=_('VLAN'), ) role = DynamicModelChoiceField( From 026386db50cd3bff1f1a17c64db07461b369f779 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Sep 2023 14:13:55 -0400 Subject: [PATCH 025/160] Fixes #13706: Restore extra filters dropdown on device interfaces list --- docs/release-notes/version-3.6.md | 1 + netbox/templates/dcim/device/interfaces.html | 4 ++++ netbox/templates/generic/object_children.html | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index e0b13f4ae..71ca249a6 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#13706](https://github.com/netbox-community/netbox/issues/13706) - Restore extra filters dropdown on device interfaces list * [#13721](https://github.com/netbox-community/netbox/issues/13721) - Filter VLAN choices by selected site (if any) when creating a prefix --- diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index cab46886b..8b3fe3097 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -2,6 +2,10 @@ {% load helpers %} {% load i18n %} +{% block table_controls %} + {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %} +{% endblock table_controls %} + {% block bulk_delete_controls %} {{ block.super }} {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} diff --git a/netbox/templates/generic/object_children.html b/netbox/templates/generic/object_children.html index 0fa59d1ec..3e93a7756 100644 --- a/netbox/templates/generic/object_children.html +++ b/netbox/templates/generic/object_children.html @@ -3,7 +3,9 @@ {% load i18n %} {% block content %} - {% include 'inc/table_controls_htmx.html' with table_modal=table_config %} + {% block table_controls %} + {% include 'inc/table_controls_htmx.html' with table_modal=table_config %} + {% endblock table_controls %}
{% csrf_token %}
From 2ffa6d0188862289850ec391769962d3ceb69221 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Sep 2023 14:16:29 -0400 Subject: [PATCH 026/160] Fixes #13701: Correct display of power feed legs under device view --- docs/release-notes/version-3.6.md | 1 + netbox/templates/dcim/device.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 71ca249a6..a019cbe13 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#13701](https://github.com/netbox-community/netbox/issues/13701) - Correct display of power feed legs under device view * [#13706](https://github.com/netbox-community/netbox/issues/13706) - Restore extra filters dropdown on device interfaces list * [#13721](https://github.com/netbox-community/netbox/issues/13721) - Filter VLAN choices by selected site (if any) when creating a prefix diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index aeab6e399..5fa6a3314 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -296,7 +296,7 @@ {% for leg in utilization.legs %} - {% trans "Leg" context "Leg of a power feed" %} {{ leg }} + {% trans "Leg" context "Leg of a power feed" %} {{ leg.name }} {{ leg.outlet_count }} {{ leg.allocated }} From 75b71890a4bcec91706cd16b9c01c61b65163fe3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Sep 2023 15:59:50 -0400 Subject: [PATCH 027/160] Misc i18n cleanup --- netbox/core/choices.py | 4 +- netbox/core/data_backends.py | 4 +- netbox/dcim/forms/bulk_import.py | 40 ++++++++++--------- netbox/dcim/models/device_components.py | 21 +++++----- netbox/dcim/models/power.py | 9 ++++- netbox/extras/forms/bulk_import.py | 2 +- netbox/extras/forms/model_forms.py | 10 +++-- netbox/extras/models/configs.py | 4 +- netbox/ipam/models/ip.py | 16 +------- netbox/templates/dcim/cable_trace.html | 6 +-- netbox/templates/dcim/powerfeed.html | 2 +- netbox/templates/exceptions/import_error.html | 15 +++---- .../exceptions/permission_error.html | 6 +-- .../exceptions/programming_error.html | 12 +++--- netbox/templates/generic/bulk_import.html | 2 +- 15 files changed, 77 insertions(+), 76 deletions(-) diff --git a/netbox/core/choices.py b/netbox/core/choices.py index 0067dfed8..b5d9d0d90 100644 --- a/netbox/core/choices.py +++ b/netbox/core/choices.py @@ -14,8 +14,8 @@ class DataSourceTypeChoices(ChoiceSet): CHOICES = ( (LOCAL, _('Local'), 'gray'), - (GIT, _('Git'), 'blue'), - (AMAZON_S3, _('Amazon S3'), 'blue'), + (GIT, 'Git', 'blue'), + (AMAZON_S3, 'Amazon S3', 'blue'), ) diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py index d2dacbbe0..82b3962dd 100644 --- a/netbox/core/data_backends.py +++ b/netbox/core/data_backends.py @@ -81,13 +81,13 @@ class GitBackend(DataBackend): required=False, label=_('Username'), widget=forms.TextInput(attrs={'class': 'form-control'}), - help_text=_("Only used for cloning with HTTP / HTTPS"), + help_text=_("Only used for cloning with HTTP(S)"), ), 'password': forms.CharField( required=False, label=_('Password'), widget=forms.TextInput(attrs={'class': 'form-control'}), - help_text=_("Only used for cloning with HTTP / HTTPS"), + help_text=_("Only used for cloning with HTTP(S)"), ), 'branch': forms.CharField( required=False, diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index a8e75e3c2..74af0696b 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -118,7 +118,9 @@ class SiteImportForm(NetBoxModelImportForm): ) help_texts = { 'time_zone': mark_safe( - _('Time zone (available options)') + '{} ({})'.format( + _('Time zone'), _('available options') + ) ) } @@ -165,7 +167,7 @@ class RackRoleImportForm(NetBoxModelImportForm): model = RackRole fields = ('name', 'slug', 'color', 'description', 'tags') help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00)')), + 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), } @@ -375,7 +377,7 @@ class DeviceRoleImportForm(NetBoxModelImportForm): model = DeviceRole fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags') help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00)')), + 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), } @@ -790,7 +792,9 @@ class InterfaceImportForm(NetBoxModelImportForm): queryset=VirtualDeviceContext.objects.all(), required=False, to_field_name='name', - help_text=_('VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")') + help_text=mark_safe( + _('VDC names separated by commas, encased with double quotes. Example:') + ' vdc1,vdc2,vdc3' + ) ) type = CSVChoiceField( label=_('Type'), @@ -1085,7 +1089,7 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm): model = InventoryItemRole fields = ('name', 'slug', 'color', 'description') help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00)')), + 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), } @@ -1096,38 +1100,38 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm): class CableImportForm(NetBoxModelImportForm): # Termination A side_a_device = CSVModelChoiceField( - label=_('Side a device'), + label=_('Side A device'), queryset=Device.objects.all(), to_field_name='name', - help_text=_('Side A device') + help_text=_('Device name') ) side_a_type = CSVContentTypeField( - label=_('Side a type'), + label=_('Side A type'), queryset=ContentType.objects.all(), limit_choices_to=CABLE_TERMINATION_MODELS, - help_text=_('Side A type') + help_text=_('Termination type') ) side_a_name = forms.CharField( - label=_('Side a name'), - help_text=_('Side A component name') + label=_('Side A name'), + help_text=_('Termination name') ) # Termination B side_b_device = CSVModelChoiceField( - label=_('Side b device'), + label=_('Side B device'), queryset=Device.objects.all(), to_field_name='name', - help_text=_('Side B device') + help_text=_('Device name') ) side_b_type = CSVContentTypeField( - label=_('Side b type'), + label=_('Side B type'), queryset=ContentType.objects.all(), limit_choices_to=CABLE_TERMINATION_MODELS, - help_text=_('Side B type') + help_text=_('Termination type') ) side_b_name = forms.CharField( - label=_('Side b name'), - help_text=_('Side B component name') + label=_('Side B name'), + help_text=_('Termination name') ) # Cable attributes @@ -1164,7 +1168,7 @@ class CableImportForm(NetBoxModelImportForm): 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags', ] help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00)')), + 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), } def _clean_side(self, side): diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index e18f25e4f..f42ae5895 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -799,9 +799,9 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd if self.bridge and self.bridge.device != self.device: if self.device.virtual_chassis is None: raise ValidationError({ - 'bridge': _(""" - The selected bridge interface ({bridge}) belongs to a different device - ({device}).""").format(bridge=self.bridge, device=self.bridge.device) + 'bridge': _( + "The selected bridge interface ({bridge}) belongs to a different device ({device})." + ).format(bridge=self.bridge, device=self.bridge.device) }) elif self.bridge.device.virtual_chassis != self.device.virtual_chassis: raise ValidationError({ @@ -889,10 +889,10 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd # Validate untagged VLAN if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]: raise ValidationError({ - 'untagged_vlan': _(""" - The untagged VLAN ({untagged_vlan}) must belong to the same site as the - interface's parent device, or it must be global. - """).format(untagged_vlan=self.untagged_vlan) + 'untagged_vlan': _( + "The untagged VLAN ({untagged_vlan}) must belong to the same site as the interface's parent " + "device, or it must be global." + ).format(untagged_vlan=self.untagged_vlan) }) def save(self, *args, **kwargs): @@ -1067,9 +1067,10 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): frontport_count = self.frontports.count() if self.positions < frontport_count: raise ValidationError({ - "positions": _(""" - The number of positions cannot be less than the number of mapped front ports - ({frontport_count})""").format(frontport_count=frontport_count) + "positions": _( + "The number of positions cannot be less than the number of mapped front ports " + "({frontport_count})" + ).format(frontport_count=frontport_count) }) diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 83e5eb23a..a852ea5cd 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -174,8 +174,13 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel): # Rack must belong to same Site as PowerPanel if self.rack and self.rack.site != self.power_panel.site: - raise ValidationError(_("Rack {} ({}) and power panel {} ({}) are in different sites").format( - self.rack, self.rack.site, self.power_panel, self.power_panel.site + raise ValidationError(_( + "Rack {rack} ({site}) and power panel {powerpanel} ({powerpanel_site}) are in different sites" + ).format( + rack=self.rack, + rack_site=self.rack.site, + powerpanel=self.power_panel, + powerpanel_site=self.power_panel.site )) # AC voltage cannot be negative diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 466baa241..79023a74d 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -164,7 +164,7 @@ class TagImportForm(CSVModelForm): model = Tag fields = ('name', 'slug', 'color', 'description') help_texts = { - 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00)')), + 'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' 00ff00'), } diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index d4e59c170..9fa7adb99 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -4,6 +4,7 @@ from django import forms from django.conf import settings from django.db.models import Q from django.contrib.contenttypes.models import ContentType +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from core.forms.mixins import SyncedDataMixin @@ -81,7 +82,8 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Disable changing the type of a CustomField as it almost universally causes errors if custom field data is already present. + # Disable changing the type of a CustomField as it almost universally causes errors if custom field data + # is already present. if self.instance.pk: self.fields['type'].disabled = True @@ -90,10 +92,10 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): extra_choices = forms.CharField( widget=ChoicesWidget(), required=False, - help_text=_( + help_text=mark_safe(_( 'Enter one choice per line. An optional label may be specified for each choice by appending it with a ' - 'comma (for example, "choice1,First Choice").' - ) + 'comma. Example:' + ) + ' choice1,First Choice') ) class Meta: diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index 47e8dcd82..2acfcb725 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -146,7 +146,7 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel): # Verify that JSON data is provided as an object if type(self.data) is not dict: raise ValidationError( - {'data': _('JSON data must be in object form. Example: {"foo": 123}')} + {'data': _('JSON data must be in object form. Example:') + ' {"foo": 123}'} ) def sync_data(self): @@ -202,7 +202,7 @@ class ConfigContextModel(models.Model): # Verify that JSON data is provided as an object if self.local_context_data and type(self.local_context_data) is not dict: raise ValidationError( - {'local_context_data': _('JSON data must be in object form. Example: {"foo": 123}')} + {'local_context_data': _('JSON data must be in object form. Example:') + ' {"foo": 123}'} ) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 89977704a..2456fa021 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -554,25 +554,13 @@ class IPRange(PrimaryModel): # Check that start & end IP versions match if self.start_address.version != self.end_address.version: raise ValidationError({ - 'end_address': _( - "Ending address version (IPv{end_address_version}) does not match starting address " - "(IPv{start_address_version})" - ).format( - end_address_version=self.end_address.version, - start_address_version=self.start_address.version - ) + 'end_address': _("Starting and ending IP address versions must match") }) # Check that the start & end IP prefix lengths match if self.start_address.prefixlen != self.end_address.prefixlen: raise ValidationError({ - 'end_address': _( - "Ending address mask (/{end_address_prefixlen}) does not match starting address mask " - "(/{start_address_prefixlen})" - ).format( - end_address_prefixlen=self.end_address.prefixlen, - start_address_prefixlen=self.start_address.prefixlen - ) + 'end_address': _("Starting and ending IP address masks must match") }) # Check that the ending address is greater than the starting address diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 12000f09d..f955c9cf8 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -51,10 +51,10 @@ {% trans "Total length" %} {% if total_length %} - {{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} {% trans "Meters" %} / - {{ total_length|meters_to_feet|floatformat:"-2" }} {% trans "Feet" %} + {{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} {% trans "Meters" %} / + {{ total_length|meters_to_feet|floatformat:"-2" }} {% trans "Feet" %} {% else %} - {% trans "N/A" %} + {{ ''|placeholder }} {% endif %} diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index ce00f333c..9b791d0e2 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -73,7 +73,7 @@ {% endif %} {% else %} - {% trans "N/A" %} + {{ ''|placeholder }} {% endif %} {% endwith %} diff --git a/netbox/templates/exceptions/import_error.html b/netbox/templates/exceptions/import_error.html index 70896328d..85803da0a 100644 --- a/netbox/templates/exceptions/import_error.html +++ b/netbox/templates/exceptions/import_error.html @@ -7,19 +7,20 @@

+ {% trans "Missing required packages" %}. {% blocktrans %} - Missing required packages. This installation of NetBox might be missing one or more required - Python packages. These packages are listed in requirements.txt and - local_requirements.txt, and are normally installed as part of the installation or upgrade process. - To verify installed packages, run pip freeze from the console and compare the output to the list of - required packages. + This installation of NetBox might be missing one or more required Python packages. These packages are listed in + requirements.txt and local_requirements.txt, and are normally installed as part of the + installation or upgrade process. To verify installed packages, run pip freeze from the console and + compare the output to the list of required packages. {% endblocktrans %}

+ {% trans "WSGI service not restarted after upgrade" %}. {% blocktrans %} - WSGI service not restarted after upgrade. If this installation has recently been upgraded, check - that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This ensures that the new code is running. + If this installation has recently been upgraded, check that the WSGI service (e.g. gunicorn or uWSGI) has been + restarted. This ensures that the new code is running. {% endblocktrans %}

{% endblock message %} diff --git a/netbox/templates/exceptions/permission_error.html b/netbox/templates/exceptions/permission_error.html index 3df6ad5c8..334c3d0bd 100644 --- a/netbox/templates/exceptions/permission_error.html +++ b/netbox/templates/exceptions/permission_error.html @@ -7,10 +7,10 @@

+ {% trans "Insufficient write permission to the media root" %}. {% blocktrans with media_root=settings.MEDIA_ROOT %} - Insufficient write permission to the media root. The configured media root is - {{ media_root }}. Ensure that the user NetBox runs as has access to write files to all locations - within this path. + The configured media root is {{ media_root }}. Ensure that the user NetBox runs as has access to + write files to all locations within this path. {% endblocktrans %}

{% endblock message %} diff --git a/netbox/templates/exceptions/programming_error.html b/netbox/templates/exceptions/programming_error.html index 5d82e4511..d24378f7c 100644 --- a/netbox/templates/exceptions/programming_error.html +++ b/netbox/templates/exceptions/programming_error.html @@ -7,18 +7,18 @@

+ {% trans "Database migrations missing" %}. {% blocktrans %} - Database migrations missing. When upgrading to a new NetBox release, the upgrade script must be - run to apply any new database migrations. You can run migrations manually by executing - python3 manage.py migrate from the command line. + When upgrading to a new NetBox release, the upgrade script must be run to apply any new database migrations. You + can run migrations manually by executing python3 manage.py migrate from the command line. {% endblocktrans %}

+ {% trans "Unsupported PostgreSQL version" %}. {% blocktrans %} - Unsupported PostgreSQL version. Ensure that PostgreSQL version 12 or later is in use. You can - check this by connecting to the database using NetBox's credentials and issuing a query for - SELECT VERSION(). + Ensure that PostgreSQL version 12 or later is in use. You can check this by connecting to the database using + NetBox's credentials and issuing a query for SELECT VERSION(). {% endblocktrans %}

{% endblock message %} diff --git a/netbox/templates/generic/bulk_import.html b/netbox/templates/generic/bulk_import.html index 08f13765c..f7d8aae77 100644 --- a/netbox/templates/generic/bulk_import.html +++ b/netbox/templates/generic/bulk_import.html @@ -177,7 +177,7 @@ Context: {% if field|widget_type == 'dateinput' %} {% trans "Format: YYYY-MM-DD" %} {% elif field|widget_type == 'checkboxinput' %} - {% trans "Specify \"true\" or \"false" %}" + {% trans "Specify true or false" %} {% endif %} From 39cb9c32d6dc0fc0e74b3827d1bc1256b448ac22 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 11 Sep 2023 16:17:02 -0400 Subject: [PATCH 028/160] Clean up blocktrans template tags (i18n) --- docs/development/internationalization.md | 4 ++-- .../circuits/circuit_terminations_swap.html | 10 +++++++--- netbox/templates/dcim/bulk_disconnect.html | 2 +- netbox/templates/dcim/cable_trace.html | 2 +- netbox/templates/dcim/devicebay_delete.html | 10 ++++++++-- netbox/templates/dcim/devicebay_depopulate.html | 4 ++-- .../dcim/devicetype/component_templates.html | 2 +- .../dcim/moduletype/component_templates.html | 2 +- netbox/templates/dcim/rack/base.html | 2 +- netbox/templates/dcim/virtualchassis_add_member.html | 6 +++++- netbox/templates/dcim/virtualchassis_edit.html | 2 +- .../templates/dcim/virtualchassis_remove_member.html | 2 +- netbox/templates/exceptions/import_error.html | 4 ++-- netbox/templates/exceptions/permission_error.html | 2 +- netbox/templates/exceptions/programming_error.html | 4 ++-- netbox/templates/extras/dashboard/reset.html | 12 ++++++++++-- .../extras/dashboard/widgets/bookmarks.html | 2 +- netbox/templates/extras/objectchange.html | 6 +++++- netbox/templates/extras/report_list.html | 2 +- netbox/templates/extras/script_list.html | 4 ++-- netbox/templates/generic/bulk_delete.html | 2 +- netbox/templates/generic/bulk_import.html | 8 ++++++-- netbox/templates/generic/bulk_remove.html | 6 +++--- netbox/templates/generic/object_edit.html | 2 +- netbox/templates/generic/object_list.html | 2 +- netbox/templates/htmx/delete_form.html | 2 +- netbox/templates/inc/missing_prerequisites.html | 2 +- netbox/templates/inc/paginator.html | 2 +- netbox/templates/inc/paginator_htmx.html | 2 +- netbox/templates/media_failure.html | 8 ++++---- .../virtualization/cluster_add_devices.html | 2 +- 31 files changed, 76 insertions(+), 46 deletions(-) diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md index bdc7cbdaa..bebc97470 100644 --- a/docs/development/internationalization.md +++ b/docs/development/internationalization.md @@ -97,7 +97,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): 1. Ensure translation support is enabled by including `{% load i18n %}` at the top of the template. 2. Use the [`{% trans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#translate-template-tag) tag (short for "translate") to wrap short strings. -3. Longer strings may be enclosed between [`{% blocktrans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#blocktranslate-template-tag) and `{% endblocktrans %}` tags to improve readability and to enable variable replacement. +3. Longer strings may be enclosed between [`{% blocktrans %}`](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#blocktranslate-template-tag) and `{% endblocktrans %}` tags to improve readability and to enable variable replacement. (Remember to include the `trimmed` argument to trim whitespace between the tags.) 4. Avoid passing HTML within translated strings where possible, as this can complicate the work needed of human translators to develop message maps. ``` @@ -107,7 +107,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
{% trans "Circuit List" %}
{# A longer string with a context variable #} -{% blocktrans with count=object.circuits.count %} +{% blocktrans trimmed with count=object.circuits.count %} There are {count} circuits. Would you like to continue? {% endblocktrans %} ``` diff --git a/netbox/templates/circuits/circuit_terminations_swap.html b/netbox/templates/circuits/circuit_terminations_swap.html index 7c9094d42..1ddb67bac 100644 --- a/netbox/templates/circuits/circuit_terminations_swap.html +++ b/netbox/templates/circuits/circuit_terminations_swap.html @@ -4,14 +4,18 @@ {% block title %}{% trans "Swap Circuit Terminations" %}{% endblock %} {% block message %} -

{% blocktrans %}Swap these terminations for circuit {{ circuit }}?{% endblocktrans %}

+

+ {% blocktrans trimmed %} + Swap these terminations for circuit {{ circuit }}? + {% endblocktrans %} +

  • {% trans "A side" %}: {% if termination_a %} {{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %} {% else %} - {{ ''|placeholder }} + {% trans "None" %} {% endif %}
  • @@ -19,7 +23,7 @@ {% if termination_z %} {{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %} {% else %} - {{ ''|placeholder }} + {% trans "None" %} {% endif %}
diff --git a/netbox/templates/dcim/bulk_disconnect.html b/netbox/templates/dcim/bulk_disconnect.html index ede0df357..555ed635b 100644 --- a/netbox/templates/dcim/bulk_disconnect.html +++ b/netbox/templates/dcim/bulk_disconnect.html @@ -6,7 +6,7 @@ {% block message %}

- {% blocktrans with count=selected_objects|length %} + {% blocktrans trimmed with count=selected_objects|length %} Are you sure you want to disconnect these {{ count }} {{ obj_type_plural }}? {% endblocktrans %}

diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index f955c9cf8..676f8a3e5 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -3,7 +3,7 @@ {% load i18n %} {% block title %} - {% blocktrans with object_type=object|meta:"verbose_name"|bettertitle %} + {% blocktrans trimmed with object_type=object|meta:"verbose_name"|bettertitle %} Cable Trace for {{ object_type }} {{ object }} {% endblocktrans %} {% endblock %} diff --git a/netbox/templates/dcim/devicebay_delete.html b/netbox/templates/dcim/devicebay_delete.html index 47e2ba545..18f4f6576 100644 --- a/netbox/templates/dcim/devicebay_delete.html +++ b/netbox/templates/dcim/devicebay_delete.html @@ -2,8 +2,14 @@ {% load form_helpers %} {% load i18n %} -{% block title %}{% blocktrans %}Delete device bay {{ devicebay }}?{% endblocktrans %}{% endblock %} +{% block title %} + {% blocktrans %}Delete device bay {{ devicebay }}?{% endblocktrans %} +{% endblock %} {% block message %} -

{% blocktrans %}Are you sure you want to delete this device bay from {{ devicebay.device }}?{% endblocktrans %}

+

+ {% blocktrans trimmed %} + Are you sure you want to delete this device bay from {{ devicebay.device }}? + {% endblocktrans %} +

{% endblock %} diff --git a/netbox/templates/dcim/devicebay_depopulate.html b/netbox/templates/dcim/devicebay_depopulate.html index a0c026800..b094f5993 100644 --- a/netbox/templates/dcim/devicebay_depopulate.html +++ b/netbox/templates/dcim/devicebay_depopulate.html @@ -3,14 +3,14 @@ {% load i18n %} {% block title %} - {% blocktrans with device=device_bay.installed_device %} + {% blocktrans trimmed with device=device_bay.installed_device %} Remove {{ device }} from {{ device_bay }}? {% endblocktrans %} {% endblock %} {% block message %}

- {% blocktrans with device=device_bay.installed_device %} + {% blocktrans trimmed with device=device_bay.installed_device %} Are you sure you want to remove {{ device }} from {{ device_bay }}? {% endblocktrans %}

diff --git a/netbox/templates/dcim/devicetype/component_templates.html b/netbox/templates/dcim/devicetype/component_templates.html index a2dcb6c0e..9a5210762 100644 --- a/netbox/templates/dcim/devicetype/component_templates.html +++ b/netbox/templates/dcim/devicetype/component_templates.html @@ -27,7 +27,7 @@
diff --git a/netbox/templates/dcim/moduletype/component_templates.html b/netbox/templates/dcim/moduletype/component_templates.html index 63cc1bb99..bb54a33f9 100644 --- a/netbox/templates/dcim/moduletype/component_templates.html +++ b/netbox/templates/dcim/moduletype/component_templates.html @@ -27,7 +27,7 @@
diff --git a/netbox/templates/dcim/rack/base.html b/netbox/templates/dcim/rack/base.html index 27ac284a2..2f4eb227c 100644 --- a/netbox/templates/dcim/rack/base.html +++ b/netbox/templates/dcim/rack/base.html @@ -1,7 +1,7 @@ {% extends 'generic/object.html' %} {% load i18n %} -{% block title %}{% blocktrans %}Rack {{ object }}{% endblocktrans %}{% endblock %} +{% block title %}{% trans "Rack" %} {{ object }}{% endblock %} {% block breadcrumbs %} {{ block.super }} diff --git a/netbox/templates/dcim/virtualchassis_add_member.html b/netbox/templates/dcim/virtualchassis_add_member.html index 6f9b24183..ceb2c71b3 100644 --- a/netbox/templates/dcim/virtualchassis_add_member.html +++ b/netbox/templates/dcim/virtualchassis_add_member.html @@ -2,7 +2,11 @@ {% load form_helpers %} {% load i18n %} -{% block title %}{% blocktrans %}Add New Member to Virtual Chassis {{ virtual_chassis }}{% endblocktrans %}{% endblock %} +{% block title %} + {% blocktrans trimmed %} + Add New Member to Virtual Chassis {{ virtual_chassis }} + {% endblocktrans %} +{% endblock %} {% block content %} diff --git a/netbox/templates/dcim/virtualchassis_edit.html b/netbox/templates/dcim/virtualchassis_edit.html index cfc3de2ec..b8f232fc2 100644 --- a/netbox/templates/dcim/virtualchassis_edit.html +++ b/netbox/templates/dcim/virtualchassis_edit.html @@ -4,7 +4,7 @@ {% load i18n %} {% block title %} - {% blocktrans with name=vc_form.instance %} + {% blocktrans trimmed with name=vc_form.instance %} Editing Virtual Chassis {{ name }} {% endblocktrans %} {% endblock %} diff --git a/netbox/templates/dcim/virtualchassis_remove_member.html b/netbox/templates/dcim/virtualchassis_remove_member.html index 520f3d862..363c2b195 100644 --- a/netbox/templates/dcim/virtualchassis_remove_member.html +++ b/netbox/templates/dcim/virtualchassis_remove_member.html @@ -6,7 +6,7 @@ {% block message %}

- {% blocktrans with name=device.virtual_chassis %} + {% blocktrans trimmed with name=device.virtual_chassis %} Are you sure you want to remove {{ device }} from virtual chassis {{ name }}? {% endblocktrans %}

diff --git a/netbox/templates/exceptions/import_error.html b/netbox/templates/exceptions/import_error.html index 85803da0a..1996412e1 100644 --- a/netbox/templates/exceptions/import_error.html +++ b/netbox/templates/exceptions/import_error.html @@ -8,7 +8,7 @@

{% trans "Missing required packages" %}. - {% blocktrans %} + {% blocktrans trimmed %} This installation of NetBox might be missing one or more required Python packages. These packages are listed in requirements.txt and local_requirements.txt, and are normally installed as part of the installation or upgrade process. To verify installed packages, run pip freeze from the console and @@ -18,7 +18,7 @@

{% trans "WSGI service not restarted after upgrade" %}. - {% blocktrans %} + {% blocktrans trimmed %} If this installation has recently been upgraded, check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This ensures that the new code is running. {% endblocktrans %} diff --git a/netbox/templates/exceptions/permission_error.html b/netbox/templates/exceptions/permission_error.html index 334c3d0bd..778508117 100644 --- a/netbox/templates/exceptions/permission_error.html +++ b/netbox/templates/exceptions/permission_error.html @@ -8,7 +8,7 @@

{% trans "Insufficient write permission to the media root" %}. - {% blocktrans with media_root=settings.MEDIA_ROOT %} + {% blocktrans trimmed with media_root=settings.MEDIA_ROOT %} The configured media root is {{ media_root }}. Ensure that the user NetBox runs as has access to write files to all locations within this path. {% endblocktrans %} diff --git a/netbox/templates/exceptions/programming_error.html b/netbox/templates/exceptions/programming_error.html index d24378f7c..fdcbcbda0 100644 --- a/netbox/templates/exceptions/programming_error.html +++ b/netbox/templates/exceptions/programming_error.html @@ -8,7 +8,7 @@

{% trans "Database migrations missing" %}. - {% blocktrans %} + {% blocktrans trimmed %} When upgrading to a new NetBox release, the upgrade script must be run to apply any new database migrations. You can run migrations manually by executing python3 manage.py migrate from the command line. {% endblocktrans %} @@ -16,7 +16,7 @@

{% trans "Unsupported PostgreSQL version" %}. - {% blocktrans %} + {% blocktrans trimmed %} Ensure that PostgreSQL version 12 or later is in use. You can check this by connecting to the database using NetBox's credentials and issuing a query for SELECT VERSION(). {% endblocktrans %} diff --git a/netbox/templates/extras/dashboard/reset.html b/netbox/templates/extras/dashboard/reset.html index ceb032c0d..b163cabb7 100644 --- a/netbox/templates/extras/dashboard/reset.html +++ b/netbox/templates/extras/dashboard/reset.html @@ -4,6 +4,14 @@ {% block title %}{% trans "Reset Dashboard" %}?{% endblock %} {% block message %} -

{% blocktrans %}This will remove all configured widgets and restore the default dashboard configuration.{% endblocktrans %}

-

{% blocktrans %}This change affects only your dashboard, and will not impact other users.{% endblocktrans %}

+

+ {% blocktrans trimmed %} + This will remove all configured widgets and restore the default dashboard configuration. + {% endblocktrans %} +

+

+ {% blocktrans trimmed %} + This change affects only your dashboard, and will not impact other users. + {% endblocktrans %} +

{% endblock %} diff --git a/netbox/templates/extras/dashboard/widgets/bookmarks.html b/netbox/templates/extras/dashboard/widgets/bookmarks.html index e8638d20e..80eb6238e 100644 --- a/netbox/templates/extras/dashboard/widgets/bookmarks.html +++ b/netbox/templates/extras/dashboard/widgets/bookmarks.html @@ -11,6 +11,6 @@ {% else %}

- {% blocktrans %}No bookmarks have been added yet.{% endblocktrans %} + {% trans "No bookmarks have been added yet." %}

{% endif %} diff --git a/netbox/templates/extras/objectchange.html b/netbox/templates/extras/objectchange.html index d681ecd75..63f2019ae 100644 --- a/netbox/templates/extras/objectchange.html +++ b/netbox/templates/extras/objectchange.html @@ -153,7 +153,11 @@ {% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %} {% if related_changes_count > related_changes_table.rows|length %} {% endif %}
diff --git a/netbox/templates/extras/report_list.html b/netbox/templates/extras/report_list.html index 81b5beb3b..49353f9cc 100644 --- a/netbox/templates/extras/report_list.html +++ b/netbox/templates/extras/report_list.html @@ -117,7 +117,7 @@

{% trans "No Reports Found" %}

{% if perms.extras.add_reportmodule %} {% url 'extras:reportmodule_add' as create_report_url %} - {% blocktrans %} + {% blocktrans trimmed %} Get started by creating a report from an uploaded file or data source. {% endblocktrans %} {% endif %} diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index c78aedc1c..fc45bebc7 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -41,7 +41,7 @@ {% if not module.scripts %} @@ -91,7 +91,7 @@

{% trans "No Scripts Found" %}

{% if perms.extras.add_scriptmodule %} {% url 'extras:scriptmodule_add' as create_script_url %} - {% blocktrans %} + {% blocktrans trimmed %} Get started by creating a script from an uploaded file or data source. {% endblocktrans %} {% endif %} diff --git a/netbox/templates/generic/bulk_delete.html b/netbox/templates/generic/bulk_delete.html index 58b0e83ee..47aca4171 100644 --- a/netbox/templates/generic/bulk_delete.html +++ b/netbox/templates/generic/bulk_delete.html @@ -24,7 +24,7 @@ Context:

{% trans "Confirm Bulk Deletion" %}


{% trans "Warning" context "Noun" %}: - {% blocktrans with count=table.rows|length type_plural=model|meta:"verbose_name_plural" %} + {% blocktrans trimmed with count=table.rows|length type_plural=model|meta:"verbose_name_plural" %} The following operation will delete {{ count }} {{ type_plural }}. Please carefully review the objects to be deleted and confirm below. {% endblocktrans %} diff --git a/netbox/templates/generic/bulk_import.html b/netbox/templates/generic/bulk_import.html index f7d8aae77..69c98f7ac 100644 --- a/netbox/templates/generic/bulk_import.html +++ b/netbox/templates/generic/bulk_import.html @@ -189,11 +189,15 @@ Context:

- {% blocktrans %}Required fields must be specified for all objects.{% endblocktrans %} + {% blocktrans trimmed %} + Required fields must be specified for all objects. + {% endblocktrans %}

- {% blocktrans with example="vrf.rd" %}Related objects may be referenced by any unique attribute. For example, {{ example }} would identify a VRF by its route distinguisher.{% endblocktrans %} + {% blocktrans trimmed with example="vrf.rd" %} + Related objects may be referenced by any unique attribute. For example, {{ example }} would identify a VRF by its route distinguisher. + {% endblocktrans %}

{% endif %} diff --git a/netbox/templates/generic/bulk_remove.html b/netbox/templates/generic/bulk_remove.html index 2691fbd3a..0c76897db 100644 --- a/netbox/templates/generic/bulk_remove.html +++ b/netbox/templates/generic/bulk_remove.html @@ -12,13 +12,13 @@